@peerbit/server 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/LICENSE +202 -0
  2. package/lib/esm/api.d.ts +33 -0
  3. package/lib/esm/api.js +370 -0
  4. package/lib/esm/api.js.map +1 -0
  5. package/lib/esm/aws.d.ts +9 -0
  6. package/lib/esm/aws.js +39 -0
  7. package/lib/esm/aws.js.map +1 -0
  8. package/lib/esm/bin.d.ts +2 -0
  9. package/lib/esm/bin.js +9 -0
  10. package/lib/esm/bin.js.map +1 -0
  11. package/lib/esm/cli.d.ts +9 -0
  12. package/lib/esm/cli.js +255 -0
  13. package/lib/esm/cli.js.map +1 -0
  14. package/lib/esm/client.d.ts +2 -0
  15. package/lib/esm/client.js +20 -0
  16. package/lib/esm/client.js.map +1 -0
  17. package/lib/esm/config.d.ts +5 -0
  18. package/lib/esm/config.js +17 -0
  19. package/lib/esm/config.js.map +1 -0
  20. package/lib/esm/docker.d.ts +2 -0
  21. package/lib/esm/docker.js +63 -0
  22. package/lib/esm/docker.js.map +1 -0
  23. package/lib/esm/domain.d.ts +10 -0
  24. package/lib/esm/domain.js +132 -0
  25. package/lib/esm/domain.js.map +1 -0
  26. package/lib/esm/index.d.ts +1 -0
  27. package/lib/esm/index.js +2 -0
  28. package/lib/esm/index.js.map +1 -0
  29. package/lib/esm/nginx-template.conf +148 -0
  30. package/lib/esm/package.json +3 -0
  31. package/lib/ui/assets/index-5265c558.css +1 -0
  32. package/lib/ui/assets/index-b451191e.js +80 -0
  33. package/lib/ui/assets/index-bd766cd9.js +3 -0
  34. package/lib/ui/assets/logo192-5b1fb15f.png +0 -0
  35. package/lib/ui/favicon-128.png +0 -0
  36. package/lib/ui/favicon-16.png +0 -0
  37. package/lib/ui/favicon-256.png +0 -0
  38. package/lib/ui/favicon-32.png +0 -0
  39. package/lib/ui/favicon-48.png +0 -0
  40. package/lib/ui/favicon-96.png +0 -0
  41. package/lib/ui/favicon.ico +0 -0
  42. package/lib/ui/index.html +36 -0
  43. package/lib/ui/logo192.png +0 -0
  44. package/lib/ui/logo512.png +0 -0
  45. package/lib/ui/manifest.json +25 -0
  46. package/lib/ui/robots.txt +3 -0
  47. package/package.json +61 -0
  48. package/src/api.ts +433 -0
  49. package/src/aws.ts +48 -0
  50. package/src/bin.ts +7 -0
  51. package/src/cli.ts +266 -0
  52. package/src/client.ts +20 -0
  53. package/src/config.ts +20 -0
  54. package/src/docker.ts +67 -0
  55. package/src/domain.ts +179 -0
  56. package/src/index.ts +1 -0
  57. package/src/nginx-template.conf +148 -0
package/src/aws.ts ADDED
@@ -0,0 +1,48 @@
1
+ import { getMyIp } from "./domain.js";
2
+
3
+ export const createRecord = async (options: {
4
+ domain: string;
5
+ region?: string;
6
+ hostedZoneId: string;
7
+ credentials?: { accessKeyId: string; secretAccessKey: string };
8
+ }): Promise<void> => {
9
+ const { Route53Client, ChangeResourceRecordSetsCommand } = await import(
10
+ "@aws-sdk/client-route-53"
11
+ );
12
+ const { isIPv4, isIPv6 } = await import("net");
13
+
14
+ const myIp = await getMyIp();
15
+ const v4 = isIPv4(myIp);
16
+ const v6 = isIPv6(myIp);
17
+
18
+ if (!v6 && !v4) {
19
+ throw new Error("Unknown ip type");
20
+ }
21
+ // TODO, make sure it works for ipv6 addresses with leading and trailing colon
22
+ const client = new Route53Client({
23
+ region: options.region,
24
+ credentials: options.credentials
25
+ ? {
26
+ accessKeyId: options.credentials.accessKeyId,
27
+ secretAccessKey: options.credentials.secretAccessKey,
28
+ }
29
+ : undefined,
30
+ });
31
+ const cmd = new ChangeResourceRecordSetsCommand({
32
+ ChangeBatch: {
33
+ Changes: [
34
+ {
35
+ Action: "CREATE",
36
+ ResourceRecordSet: {
37
+ Name: options.domain,
38
+ Type: v4 ? "A" : "AAAA",
39
+ TTL: 60,
40
+ ResourceRecords: [{ Value: myIp }],
41
+ },
42
+ },
43
+ ],
44
+ },
45
+ HostedZoneId: options.hostedZoneId,
46
+ });
47
+ await client.send(cmd);
48
+ };
package/src/bin.ts ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { cli } from "./cli.js";
3
+ try {
4
+ await cli();
5
+ } catch (error: any) {
6
+ throw new Error("Unexpected error: " + error?.message);
7
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,266 @@
1
+ import { createTestDomain, startCertbot } from "./domain.js";
2
+ import { serialize } from "@dao-xyz/borsh";
3
+ import { client, startServerWithNode } from "./api.js";
4
+ import { createRecord } from "./aws.js";
5
+ import { toBase64 } from "@peerbit/crypto";
6
+ import { getConfigDir } from "./config.js";
7
+
8
+ export const cli = async (args?: string[]) => {
9
+ const yargs = await import("yargs");
10
+
11
+ if (!args) {
12
+ const { hideBin } = await import("yargs/helpers");
13
+ args = hideBin(process.argv);
14
+ }
15
+
16
+ return yargs
17
+ .default(args)
18
+ .command({
19
+ command: "start",
20
+ describe: "Start node",
21
+ builder: {
22
+ directory: {
23
+ describe: "Directory for all data created by the node",
24
+ defaultDescription: "~.peerbit",
25
+ type: "string",
26
+ default: await getConfigDir(),
27
+ },
28
+ },
29
+ handler: async (args) => {
30
+ await startServerWithNode(args.directory);
31
+ },
32
+ })
33
+ .command("domain", "Setup a domain and certificate", (yargs) => {
34
+ yargs
35
+ .command({
36
+ command: "test",
37
+ describe:
38
+ "Setup a testing domain with SSL (no guarantess on how long the domain will be available)",
39
+ builder: {
40
+ email: {
41
+ describe: "Email for Lets encrypt autorenewal messages",
42
+ type: "string",
43
+ demandOption: true,
44
+ },
45
+ outdir: {
46
+ describe: "Output path for Nginx config",
47
+ type: "string",
48
+ alias: "o",
49
+ },
50
+ wait: {
51
+ alias: "w",
52
+ describe: "Wait for setup to succeed (or fail)",
53
+ type: "boolean",
54
+ default: false,
55
+ },
56
+ },
57
+ handler: async (args) => {
58
+ const domain = await createTestDomain();
59
+ await startCertbot(domain, args.email, args.outdir, args.wait);
60
+ const { exit } = await import("process");
61
+ exit();
62
+ },
63
+ })
64
+ .command({
65
+ command: "aws",
66
+ describe:
67
+ "Setup a domain with an AWS account. You either have to setup you AWS credentials in the .aws folder, or pass the credentials in the cli",
68
+ builder: {
69
+ domain: {
70
+ describe: "domain, e.g. abc.example.com, example.com",
71
+ alias: "d",
72
+ type: "string",
73
+ demandOption: true,
74
+ },
75
+ hostedZoneId: {
76
+ describe: 'The id of the hosted zone "HostedZoneId"',
77
+ alias: "hz",
78
+ type: "string",
79
+ require: true,
80
+ },
81
+ accessKeyId: {
82
+ describe: "Access key id of the AWS user",
83
+ alias: "ak",
84
+ type: "string",
85
+ },
86
+ region: {
87
+ describe: "AWS region",
88
+ alias: "r",
89
+ type: "string",
90
+ },
91
+ secretAccessKey: {
92
+ describe: "Secret key id of the AWS user",
93
+ alias: "sk",
94
+ type: "string",
95
+ },
96
+ email: {
97
+ describe: "Email for Lets encrypt autorenewal messages",
98
+ type: "string",
99
+ demandOption: true,
100
+ },
101
+ outdir: {
102
+ describe: "Output path for Nginx config",
103
+ type: "string",
104
+ alias: "o",
105
+ },
106
+ wait: {
107
+ alias: "w",
108
+ describe: "Wait for setup to succeed (or fail)",
109
+ type: "boolean",
110
+ default: false,
111
+ },
112
+ },
113
+ handler: async (args) => {
114
+ if (
115
+ !!args.accessKeyId !== !!args.secretAccessKey ||
116
+ !!args.region !== !!args.secretAccessKey
117
+ ) {
118
+ throw new Error(
119
+ "Expecting either all 'accessKeyId', 'region' and 'secretAccessKey' to be provided or none"
120
+ );
121
+ }
122
+ await createRecord({
123
+ domain: args.domain,
124
+ hostedZoneId: args.hostedZoneId,
125
+ region: args.region,
126
+ credentials: args.accessKeyId
127
+ ? {
128
+ accessKeyId: args.accessKeyId,
129
+ secretAccessKey: args.secretAccessKey,
130
+ }
131
+ : undefined,
132
+ });
133
+ await startCertbot(args.domain, args.email, args.outdir, args.wait);
134
+ const { exit } = await import("process");
135
+ exit();
136
+ },
137
+ })
138
+ .strict()
139
+ .demandCommand();
140
+ })
141
+ .command("topic", "Manage topics the node is listening to", (yargs) => {
142
+ yargs
143
+ .command({
144
+ command: "list",
145
+ aliases: "ls",
146
+ describe: "List all topics",
147
+ builder: (yargs: any) => {
148
+ yargs.option("replicate", {
149
+ type: "boolean",
150
+ describe: "Replicate data on this topic",
151
+ alias: "r",
152
+ default: false,
153
+ });
154
+ return yargs;
155
+ },
156
+ handler: async (args) => {
157
+ /* const c = await client();
158
+ const topics = await c.topics.get(args.replicate);
159
+ if (topics?.length > 0) {
160
+ console.log("Topic (" + topics.length + "):");
161
+ for (const t of topics) {
162
+ console.log(t);
163
+ }
164
+ } else {
165
+ console.log("Not subscribed to any topics");
166
+ } */
167
+ console.error("Not implemented");
168
+ },
169
+ })
170
+ .strict()
171
+ .demandCommand();
172
+ return yargs;
173
+ })
174
+ .command("program", "Manage programs", (yargs) => {
175
+ yargs
176
+ .command({
177
+ command: "get <address>",
178
+ describe: "Get program manifest/serialized in base64",
179
+ builder: (yargs: any) => {
180
+ yargs.positional("address", {
181
+ type: "string",
182
+ describe: "Program address",
183
+ demandOption: true,
184
+ });
185
+ return yargs;
186
+ },
187
+
188
+ handler: async (args) => {
189
+ const c = await client();
190
+ const program = await c.program.get(args.address);
191
+ if (!program) {
192
+ console.log("Program does not exist");
193
+ } else {
194
+ console.log(toBase64(serialize(program)));
195
+ }
196
+ },
197
+ })
198
+ .command({
199
+ command: "add <program>",
200
+ describe: "Add program",
201
+ builder: (yargs: any) => {
202
+ yargs.positional("program", {
203
+ type: "string",
204
+ describe: "base64 serialized",
205
+ demandOption: true,
206
+ });
207
+ return yargs;
208
+ },
209
+ handler: async (args) => {
210
+ const c = await client();
211
+ const address = await c.program.put(args.program);
212
+ console.log(address.toString());
213
+ },
214
+ })
215
+ .command({
216
+ command: "import <library>",
217
+ describe: "import a library that contains programs",
218
+ builder: (yargs: any) => {
219
+ yargs.positional("library", {
220
+ type: "array",
221
+ describe:
222
+ "Library name (will be loaded with js import(...)). Onlu libraries that are globally installed and can be imported",
223
+ demandOption: true,
224
+ });
225
+ return yargs;
226
+ },
227
+ handler: async (args) => {
228
+ for (const lib of args.library) {
229
+ const importedLib = await import(
230
+ /* webpackIgnore: true */ /* @vite-ignore */ lib
231
+ );
232
+ console.log("imported lib:", importedLib);
233
+ }
234
+ },
235
+ })
236
+ .strict()
237
+ .demandCommand();
238
+ return yargs;
239
+ })
240
+ .command("library", "Manage libraries", (yargs) => {
241
+ yargs
242
+ .command({
243
+ command: "add <library>",
244
+ describe: "add a library that contains programs",
245
+ builder: (yargs: any) => {
246
+ yargs.positional("library", {
247
+ type: "string",
248
+ describe:
249
+ "Library name (will be loaded with js import(...)). Onlu libraries that are globally installed and can be imported",
250
+ demandOption: true,
251
+ });
252
+ return yargs;
253
+ },
254
+ handler: async (args) => {
255
+ const c = await client();
256
+ await c.library.put(args.library);
257
+ },
258
+ })
259
+ .strict()
260
+ .demandCommand();
261
+ return yargs;
262
+ })
263
+ .help()
264
+ .strict()
265
+ .demandCommand().argv;
266
+ };
package/src/client.ts ADDED
@@ -0,0 +1,20 @@
1
+ import { DirectSub } from "@peerbit/pubsub";
2
+ import { Peerbit } from "peerbit";
3
+
4
+ export const create = (directory: string) => {
5
+ return Peerbit.create({
6
+ libp2p: {
7
+ addresses: {
8
+ listen: ["/ip4/127.0.0.1/tcp/8001", "/ip4/127.0.0.1/tcp/8002/ws"],
9
+ },
10
+ connectionManager: {
11
+ maxConnections: Infinity,
12
+ minConnections: 0,
13
+ },
14
+ services: {
15
+ pubsub: (c) => new DirectSub(c, { canRelayMessage: true }),
16
+ },
17
+ },
18
+ directory,
19
+ });
20
+ };
package/src/config.ts ADDED
@@ -0,0 +1,20 @@
1
+ export const getConfigDir = async (): Promise<string> => {
2
+ const path = await import("path");
3
+ const os = await import("os");
4
+ const configDir = path.join(os.homedir(), ".peerbit");
5
+ return configDir;
6
+ };
7
+
8
+ export const getCredentialsPath = async (
9
+ configDir: string
10
+ ): Promise<string> => {
11
+ const path = await import("path");
12
+ return path.join(configDir, "credentials");
13
+ };
14
+
15
+ export const getKeysPath = async (configDir: string): Promise<string> => {
16
+ const path = await import("path");
17
+ return path.join(configDir, "keys");
18
+ };
19
+
20
+ export class NotFoundError extends Error {}
package/src/docker.ts ADDED
@@ -0,0 +1,67 @@
1
+ import { delay, waitForAsync } from "@peerbit/time";
2
+
3
+ export const installDocker = async () => {
4
+ const { exec } = await import("child_process");
5
+
6
+ // check if docker is installed
7
+ const dockerExist = async () => {
8
+ try {
9
+ const out = await new Promise((resolve, reject) => {
10
+ exec("docker --version", (error, stdout, stderr) => {
11
+ if (error || stderr) {
12
+ reject();
13
+ }
14
+ resolve(stdout);
15
+ });
16
+ });
17
+ return true;
18
+ } catch (error) {
19
+ return false;
20
+ }
21
+ };
22
+
23
+ if (!(await dockerExist())) {
24
+ await new Promise((resolve, reject) => {
25
+ exec("sudo snap install docker", (error, stdout, stderr) => {
26
+ if (error || stderr) {
27
+ reject();
28
+ }
29
+ resolve(stdout);
30
+ });
31
+ });
32
+
33
+ try {
34
+ await waitForAsync(() => dockerExist(), {
35
+ timeout: 30 * 1000,
36
+ delayInterval: 1000,
37
+ });
38
+ } catch (error) {
39
+ throw new Error("Failed to install docker");
40
+ }
41
+ }
42
+ };
43
+
44
+ export const startContainer = async (cmd: string, errorMessage?: string) => {
45
+ const { exec } = await import("child_process");
46
+ const startContainer = () =>
47
+ new Promise((resolve, reject) => {
48
+ exec(cmd, (error, stdout, stderr) => {
49
+ if (error) {
50
+ reject(
51
+ (errorMessage || "Failed to start docker container: ") +
52
+ error.message
53
+ );
54
+ }
55
+ resolve(stdout);
56
+ });
57
+ });
58
+ try {
59
+ await startContainer();
60
+ } catch (error) {
61
+ // try again no matter what?
62
+ // or
63
+ // typeof error === "string" && error.indexOf("Cannot connect to the Docker daemon") != -1
64
+ await delay(10000);
65
+ await startContainer();
66
+ }
67
+ };
package/src/domain.ts ADDED
@@ -0,0 +1,179 @@
1
+ import { waitFor, waitForAsync } from "@peerbit/time";
2
+ import { client } from "./api.js";
3
+ import { installDocker, startContainer } from "./docker.js";
4
+
5
+ const isNode = typeof window === undefined || typeof window === "undefined";
6
+
7
+ const validateEmail = (email) => {
8
+ return String(email)
9
+ .toLowerCase()
10
+ .match(
11
+ /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
12
+ );
13
+ };
14
+
15
+ const createConfig = async (
16
+ outputPath: string,
17
+ domain: string
18
+ ): Promise<{ domain: string }> => {
19
+ if (!isNode) {
20
+ throw new Error("Config can only be created with node");
21
+ }
22
+ const { AxiosError } = await import("axios");
23
+
24
+ const url = await import("url");
25
+ const __filename = url.fileURLToPath(import.meta.url);
26
+ const fs = await import("fs");
27
+ const path = await import("path");
28
+ let file = fs.readFileSync(
29
+ path.join(__filename, "../nginx-template.conf"),
30
+ "utf-8"
31
+ );
32
+ let ipfsId: string;
33
+ try {
34
+ ipfsId = await (await client()).peer.id.get();
35
+ } catch (error) {
36
+ if (error instanceof AxiosError) {
37
+ throw new Error(
38
+ "Failed to connect to Peerbit, have you started the node? e.g. 'peerbit start'"
39
+ );
40
+ }
41
+ throw error;
42
+ }
43
+ file = file.replaceAll("%IPFS_ID%", ipfsId);
44
+ file = file.replaceAll("%DOMAIN%", domain);
45
+
46
+ fs.mkdir(outputPath, { recursive: true }, (err) => {
47
+ if (err) throw err;
48
+ });
49
+
50
+ await waitFor(() => fs.existsSync(outputPath));
51
+
52
+ fs.writeFileSync(path.join(outputPath, "default.conf"), file);
53
+ return { domain };
54
+ };
55
+ const getUIPath = async (): Promise<string> => {
56
+ const url = await import("url");
57
+ const path = await import("path");
58
+ const __filename = url.fileURLToPath(import.meta.url);
59
+ const p1 = path.join(__filename, "../../", "ui");
60
+
61
+ const fs = await import("fs");
62
+
63
+ if (fs.existsSync(p1) && fs.lstatSync(p1).isDirectory()) {
64
+ return p1; // build
65
+ } else {
66
+ const p2 = path.join(__filename, "../../", "lib/ui");
67
+ if (fs.existsSync(p2) && fs.lstatSync(p2).isDirectory()) {
68
+ return p2;
69
+ }
70
+ throw new Error("Failed to find UI path");
71
+ }
72
+ };
73
+ export const getMyIp = async (): Promise<string> => {
74
+ const { exec } = await import("child_process");
75
+ const ipv4: string = await new Promise((resolve, reject) => {
76
+ exec(
77
+ "dig @resolver4.opendns.com myip.opendns.com +short",
78
+ (error, stdout, stderr) => {
79
+ if (error || stderr) {
80
+ reject("DNS lookup failed");
81
+ }
82
+ resolve(stdout.trimEnd());
83
+ }
84
+ );
85
+ });
86
+ return ipv4;
87
+ };
88
+
89
+ export const createTestDomain = async () => {
90
+ const { default: axios } = await import("axios");
91
+ const domain: string = (
92
+ await axios.post(
93
+ "https://bfbbnhwpfj2ptcmurz6lit4xlu0vjajw.lambda-url.us-east-1.on.aws",
94
+ await getMyIp(),
95
+ { headers: { "Content-Type": "application/json" } }
96
+ )
97
+ ).data.domain;
98
+ return domain;
99
+ };
100
+
101
+ /**
102
+ *
103
+ * @param email
104
+ * @param nginxConfigPath
105
+ * @param dockerProcessName
106
+ * @returns domain
107
+ */
108
+ export const startCertbot = async (
109
+ domain: string,
110
+ email: string,
111
+ nginxConfigPath?: string,
112
+ waitForUp = false,
113
+ dockerProcessName = "nginx-certbot"
114
+ ): Promise<void> => {
115
+ if (!validateEmail(email)) {
116
+ throw new Error("Email for SSL renenewal is invalid");
117
+ }
118
+
119
+ const { exec } = await import("child_process");
120
+ const pwd: string = await new Promise((resolve, reject) => {
121
+ exec("pwd", (error, stdout, stderr) => {
122
+ if (error || stderr) {
123
+ reject("Failed to get current directory");
124
+ }
125
+ resolve(stdout.trimEnd());
126
+ });
127
+ });
128
+ const path = await import("path");
129
+ nginxConfigPath = path.join(nginxConfigPath || pwd, "nginx");
130
+
131
+ await createConfig(nginxConfigPath, domain);
132
+
133
+ await installDocker();
134
+
135
+ // run
136
+ const isTest = process.env.JEST_WORKER_ID !== undefined;
137
+
138
+ const uiPath = await getUIPath();
139
+
140
+ // copy ui from node_modules to home for permission reasons (volume will not work otherwise)
141
+ const certbotDockerCommand = `cp -r ${uiPath} $(pwd)/ui && docker pull jonasal/nginx-certbot:latest && docker run -d --net=host \
142
+ --env CERTBOT_EMAIL=${email} ${isTest ? "--env STAGING=1" : ""}\
143
+ -v $(pwd)/nginx_secrets:/etc/letsencrypt \
144
+ -v ${nginxConfigPath}:/etc/nginx/user_conf.d:ro \
145
+ -v $(pwd)/ui:/usr/share/nginx/html:ro \
146
+ --name ${dockerProcessName} jonasal/nginx-certbot:latest`;
147
+
148
+ console.log("Starting Certbot");
149
+ // try two times with some delay, because sometimes the docker daemon is not available immidatel
150
+ await startContainer(
151
+ certbotDockerCommand,
152
+ "Failed to start certbot container"
153
+ );
154
+
155
+ console.log("Certbot started succesfully!");
156
+ console.log("You domain is: ");
157
+ console.log(domain);
158
+ if (waitForUp) {
159
+ const { default: axios } = await import("axios");
160
+
161
+ console.log("Waiting for domain to be ready ...");
162
+ await waitForAsync(
163
+ async () => {
164
+ try {
165
+ const status = (await axios.get("https://" + domain)).status;
166
+ return status >= 200 && status < 400;
167
+ } catch (error) {
168
+ return false;
169
+ }
170
+ },
171
+ { timeout: 5 * 60 * 10000, delayInterval: 5000 }
172
+ );
173
+ console.log("Domain is ready");
174
+ } else {
175
+ console.log(
176
+ "The domain is not available immediately as it takes some time to request SSL certificate."
177
+ );
178
+ }
179
+ };
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "./api.js";