@peerbit/server 5.9.5 → 5.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/aws.d.ts.map +1 -1
- package/dist/src/aws.js +2 -1
- package/dist/src/aws.js.map +1 -1
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +142 -1
- package/dist/src/cli.js.map +1 -1
- package/dist/src/client.d.ts.map +1 -1
- package/dist/src/client.js +7 -1
- package/dist/src/client.js.map +1 -1
- package/dist/src/docker.d.ts.map +1 -1
- package/dist/src/docker.js +122 -32
- package/dist/src/docker.js.map +1 -1
- package/dist/src/hetzner.browser.d.ts +2 -0
- package/dist/src/hetzner.browser.d.ts.map +1 -0
- package/dist/src/hetzner.browser.js +3 -0
- package/dist/src/hetzner.browser.js.map +1 -0
- package/dist/src/hetzner.d.ts +26 -0
- package/dist/src/hetzner.d.ts.map +1 -0
- package/dist/src/hetzner.js +161 -0
- package/dist/src/hetzner.js.map +1 -0
- package/dist/src/remotes.d.ts +6 -1
- package/dist/src/remotes.d.ts.map +1 -1
- package/dist/src/remotes.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/ui/assets/{index-BEgs1gyX.js → index-Dst3R_bZ.js} +6 -6
- package/dist/ui/index.html +1 -1
- package/package.json +6 -5
- package/src/aws.ts +2 -1
- package/src/cli.ts +164 -1
- package/src/client.ts +6 -1
- package/src/docker.ts +139 -31
- package/src/hetzner.browser.ts +1 -0
- package/src/hetzner.ts +242 -0
- package/src/remotes.ts +7 -1
package/src/hetzner.ts
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
/* eslint-disable @typescript-eslint/naming-convention */
|
|
3
|
+
import { type PeerId } from "@libp2p/interface";
|
|
4
|
+
import { delay } from "@peerbit/time";
|
|
5
|
+
|
|
6
|
+
export const HETZNER_LOCATIONS = [
|
|
7
|
+
"fsn1",
|
|
8
|
+
"nbg1",
|
|
9
|
+
"hel1",
|
|
10
|
+
"ash",
|
|
11
|
+
"hil",
|
|
12
|
+
] as const;
|
|
13
|
+
export type HetznerLocation = (typeof HETZNER_LOCATIONS)[number];
|
|
14
|
+
|
|
15
|
+
export const HETZNER_SERVER_TYPES = [
|
|
16
|
+
"cx11",
|
|
17
|
+
"cx21",
|
|
18
|
+
"cx31",
|
|
19
|
+
"cx41",
|
|
20
|
+
"cx51",
|
|
21
|
+
"cax11",
|
|
22
|
+
"cax21",
|
|
23
|
+
"cax31",
|
|
24
|
+
"cax41",
|
|
25
|
+
] as const;
|
|
26
|
+
export type HetznerServerType = (typeof HETZNER_SERVER_TYPES)[number];
|
|
27
|
+
|
|
28
|
+
const HCLOUD_API_BASE = "https://api.hetzner.cloud/v1";
|
|
29
|
+
|
|
30
|
+
const getToken = (token?: string): string => {
|
|
31
|
+
const resolved =
|
|
32
|
+
token ||
|
|
33
|
+
process.env.HCLOUD_TOKEN ||
|
|
34
|
+
process.env.HETZNER_TOKEN ||
|
|
35
|
+
process.env.HETZNER_CLOUD_TOKEN;
|
|
36
|
+
if (!resolved) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
"Missing Hetzner Cloud API token. Provide --token or set HCLOUD_TOKEN.",
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
return resolved;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const setupUserData = (
|
|
45
|
+
email: string,
|
|
46
|
+
grantAccess: PeerId[] = [],
|
|
47
|
+
serverVersion?: string,
|
|
48
|
+
) => {
|
|
49
|
+
const peerIdStrings = grantAccess.map((x) => x.toString());
|
|
50
|
+
const grantArgs = peerIdStrings.map((key) => `--ga ${key}`).join(" ");
|
|
51
|
+
const versionSpec = serverVersion ? `@${serverVersion}` : "";
|
|
52
|
+
|
|
53
|
+
// better-sqlite3 forces users to install build-essentials for `make` command
|
|
54
|
+
return `#!/bin/bash
|
|
55
|
+
set -e
|
|
56
|
+
cd /root
|
|
57
|
+
curl -fsSL https://deb.nodesource.com/setup_22.x | bash - &&\
|
|
58
|
+
apt-get install -y nodejs
|
|
59
|
+
apt-get install -y build-essential
|
|
60
|
+
npm install -g @peerbit/server${versionSpec}
|
|
61
|
+
peerbit domain test --email ${email}
|
|
62
|
+
peerbit start ${grantArgs} > log.txt 2>&1 &
|
|
63
|
+
`;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
type HcloudServer = {
|
|
67
|
+
id: number;
|
|
68
|
+
name: string;
|
|
69
|
+
public_net?: { ipv4?: { ip?: string } };
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const createHcloudClient = async (token: string) => {
|
|
73
|
+
const { default: axios } = await import("axios");
|
|
74
|
+
return axios.create({
|
|
75
|
+
baseURL: HCLOUD_API_BASE,
|
|
76
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
77
|
+
timeout: 60_000,
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const parseAxiosError = (error: any): string => {
|
|
82
|
+
const status = error?.response?.status;
|
|
83
|
+
const data = error?.response?.data;
|
|
84
|
+
const message = error?.message || String(error);
|
|
85
|
+
if (!status) {
|
|
86
|
+
return message;
|
|
87
|
+
}
|
|
88
|
+
const details =
|
|
89
|
+
typeof data === "string"
|
|
90
|
+
? data
|
|
91
|
+
: data?.error?.message || JSON.stringify(data);
|
|
92
|
+
return `${message} (HTTP ${status})${details ? `: ${details}` : ""}`;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const escapeRegExp = (value: string): string =>
|
|
96
|
+
value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
97
|
+
|
|
98
|
+
const getHighestNameCounter = (
|
|
99
|
+
servers: HcloudServer[],
|
|
100
|
+
prefix: string,
|
|
101
|
+
): number => {
|
|
102
|
+
const pattern = new RegExp(`^${escapeRegExp(prefix)}-(\\d+)$`);
|
|
103
|
+
let max = 0;
|
|
104
|
+
for (const server of servers) {
|
|
105
|
+
const match = server.name.match(pattern);
|
|
106
|
+
if (!match) continue;
|
|
107
|
+
const n = Number(match[1]);
|
|
108
|
+
if (Number.isFinite(n)) {
|
|
109
|
+
max = Math.max(max, n);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return max;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const waitForServerPublicIp = async (
|
|
116
|
+
client: Awaited<ReturnType<typeof createHcloudClient>>,
|
|
117
|
+
serverId: number,
|
|
118
|
+
timeoutMs = 3 * 60 * 1000,
|
|
119
|
+
pollIntervalMs = 5000,
|
|
120
|
+
): Promise<string> => {
|
|
121
|
+
const start = Date.now();
|
|
122
|
+
while (Date.now() - start < timeoutMs) {
|
|
123
|
+
try {
|
|
124
|
+
const info = await client.get(`/servers/${serverId}`);
|
|
125
|
+
const server: HcloudServer | undefined = info.data?.server;
|
|
126
|
+
const ip = server?.public_net?.ipv4?.ip;
|
|
127
|
+
if (ip) {
|
|
128
|
+
return ip;
|
|
129
|
+
}
|
|
130
|
+
} catch (error: any) {
|
|
131
|
+
if (error?.response?.status !== 404 && error?.response?.status !== 429) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
`Failed while waiting for Hetzner server ${serverId} IP: ${parseAxiosError(
|
|
134
|
+
error,
|
|
135
|
+
)}`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
await delay(pollIntervalMs);
|
|
140
|
+
}
|
|
141
|
+
throw new Error(
|
|
142
|
+
`Timed out waiting for Hetzner server ${serverId} to get a public IPv4`,
|
|
143
|
+
);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
export const launchNodes = async (properties: {
|
|
147
|
+
token?: string;
|
|
148
|
+
location?: string;
|
|
149
|
+
email: string;
|
|
150
|
+
count?: number;
|
|
151
|
+
serverType?: string;
|
|
152
|
+
namePrefix?: string;
|
|
153
|
+
grantAccess?: PeerId[];
|
|
154
|
+
serverVersion?: string;
|
|
155
|
+
image?: string;
|
|
156
|
+
}): Promise<
|
|
157
|
+
{ serverId: number; publicIp: string; name: string; location: string }[]
|
|
158
|
+
> => {
|
|
159
|
+
if (properties.count && properties.count > 10) {
|
|
160
|
+
throw new Error(
|
|
161
|
+
"Unexpected node launch count: " +
|
|
162
|
+
properties.count +
|
|
163
|
+
". To prevent unwanted behaviour you can also launch 10 nodes at once",
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
const count = properties.count || 1;
|
|
167
|
+
|
|
168
|
+
const token = getToken(properties.token);
|
|
169
|
+
const client = await createHcloudClient(token);
|
|
170
|
+
|
|
171
|
+
const location = properties.location || "fsn1";
|
|
172
|
+
const serverType = properties.serverType || "cx11";
|
|
173
|
+
const image = properties.image || "ubuntu-22.04";
|
|
174
|
+
const namePrefix = properties.namePrefix || "peerbit-node";
|
|
175
|
+
|
|
176
|
+
const existingServers = (await client.get("/servers")).data?.servers as
|
|
177
|
+
| HcloudServer[]
|
|
178
|
+
| undefined;
|
|
179
|
+
const existingCounter = getHighestNameCounter(
|
|
180
|
+
existingServers || [],
|
|
181
|
+
namePrefix,
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
const created: Array<{ serverId: number; name: string }> = [];
|
|
185
|
+
for (let ix = 1; ix <= count; ix++) {
|
|
186
|
+
const name = `${namePrefix}-${existingCounter + ix}`;
|
|
187
|
+
try {
|
|
188
|
+
const resp = await client.post("/servers", {
|
|
189
|
+
name,
|
|
190
|
+
server_type: serverType,
|
|
191
|
+
image,
|
|
192
|
+
location,
|
|
193
|
+
user_data: setupUserData(
|
|
194
|
+
properties.email,
|
|
195
|
+
properties.grantAccess,
|
|
196
|
+
properties.serverVersion,
|
|
197
|
+
),
|
|
198
|
+
});
|
|
199
|
+
const serverId: number | undefined = resp.data?.server?.id;
|
|
200
|
+
if (!serverId) {
|
|
201
|
+
throw new Error("Missing server id in Hetzner response");
|
|
202
|
+
}
|
|
203
|
+
created.push({ serverId, name });
|
|
204
|
+
} catch (error: any) {
|
|
205
|
+
throw new Error(
|
|
206
|
+
`Failed to create Hetzner server '${name}': ${parseAxiosError(error)}`,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const nodes: Array<{
|
|
212
|
+
serverId: number;
|
|
213
|
+
publicIp: string;
|
|
214
|
+
name: string;
|
|
215
|
+
location: string;
|
|
216
|
+
}> = [];
|
|
217
|
+
for (const { serverId, name } of created) {
|
|
218
|
+
const publicIp = await waitForServerPublicIp(client, serverId);
|
|
219
|
+
nodes.push({ serverId, publicIp, name, location });
|
|
220
|
+
}
|
|
221
|
+
return nodes;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
export const terminateNode = async (properties: {
|
|
225
|
+
token?: string;
|
|
226
|
+
serverId: number | string;
|
|
227
|
+
}) => {
|
|
228
|
+
const token = getToken(properties.token);
|
|
229
|
+
const client = await createHcloudClient(token);
|
|
230
|
+
try {
|
|
231
|
+
await client.delete(`/servers/${properties.serverId}`);
|
|
232
|
+
} catch (error: any) {
|
|
233
|
+
if (error?.response?.status === 404) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
throw new Error(
|
|
237
|
+
`Failed to terminate Hetzner server ${properties.serverId}: ${parseAxiosError(
|
|
238
|
+
error,
|
|
239
|
+
)}`,
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
};
|
package/src/remotes.ts
CHANGED
|
@@ -6,7 +6,13 @@ interface AWSOrigin {
|
|
|
6
6
|
instanceId: string;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
interface HetznerOrigin {
|
|
10
|
+
type: "hetzner";
|
|
11
|
+
location: string;
|
|
12
|
+
serverId: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type RemoteOrigin = AWSOrigin | HetznerOrigin;
|
|
10
16
|
|
|
11
17
|
export const DEFAULT_REMOTE_GROUP = "default";
|
|
12
18
|
|