@peerbit/server 2.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/lib/esm/cli.js +67 -20
  2. package/lib/esm/cli.js.map +1 -1
  3. package/lib/esm/client.d.ts +7 -1
  4. package/lib/esm/client.js +48 -31
  5. package/lib/esm/client.js.map +1 -1
  6. package/lib/esm/config.d.ts +3 -3
  7. package/lib/esm/config.js +18 -16
  8. package/lib/esm/config.js.map +1 -1
  9. package/lib/esm/peerbit.d.ts +2 -0
  10. package/lib/esm/peerbit.js +1 -0
  11. package/lib/esm/peerbit.js.map +1 -1
  12. package/lib/esm/remotes.d.ts +1 -2
  13. package/lib/esm/remotes.js +1 -2
  14. package/lib/esm/remotes.js.map +1 -1
  15. package/lib/esm/routes.d.ts +1 -0
  16. package/lib/esm/routes.js +1 -0
  17. package/lib/esm/routes.js.map +1 -1
  18. package/lib/esm/server.d.ts +2 -6
  19. package/lib/esm/server.js +161 -176
  20. package/lib/esm/server.js.map +1 -1
  21. package/lib/esm/signes-request.d.ts +5 -0
  22. package/lib/esm/signes-request.js +54 -0
  23. package/lib/esm/signes-request.js.map +1 -0
  24. package/lib/esm/trust.browser.d.ts +0 -0
  25. package/lib/esm/trust.browser.js +3 -0
  26. package/lib/esm/trust.browser.js.map +1 -0
  27. package/lib/esm/trust.d.ts +9 -0
  28. package/lib/esm/trust.js +36 -0
  29. package/lib/esm/trust.js.map +1 -0
  30. package/lib/ui/assets/index-cac7195d.js +77 -0
  31. package/lib/ui/index.html +1 -1
  32. package/package.json +4 -4
  33. package/src/cli.ts +81 -25
  34. package/src/client.ts +77 -35
  35. package/src/config.ts +21 -23
  36. package/src/peerbit.ts +3 -0
  37. package/src/remotes.ts +1 -3
  38. package/src/routes.ts +1 -1
  39. package/src/server.ts +205 -241
  40. package/src/signes-request.ts +84 -0
  41. package/src/trust.browser.ts +1 -0
  42. package/src/trust.ts +39 -0
  43. package/lib/ui/assets/index-73eaa3bc.js +0 -53
package/src/server.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import http from "http";
2
- import { fromBase64, sha256Base64Sync } from "@peerbit/crypto";
2
+ import { Ed25519Keypair, fromBase64, sha256Base64Sync } from "@peerbit/crypto";
3
3
  import { deserialize } from "@dao-xyz/borsh";
4
4
  import {
5
5
  Program,
@@ -10,13 +10,10 @@ import {
10
10
  import { waitFor } from "@peerbit/time";
11
11
  import { v4 as uuid } from "uuid";
12
12
  import {
13
- checkExistPath,
14
13
  getHomeConfigDir,
15
- getCredentialsPath,
16
- getPackageName,
17
- loadPassword,
18
- NotFoundError,
19
14
  getNodePath,
15
+ getKeypair,
16
+ getTrustPath,
20
17
  } from "./config.js";
21
18
  import { setMaxListeners } from "events";
22
19
  import { create } from "./peerbit.js";
@@ -38,6 +35,7 @@ import {
38
35
  PROGRAMS_PATH,
39
36
  PROGRAM_PATH,
40
37
  RESTART_PATH,
38
+ TRUST_PATH,
41
39
  } from "./routes.js";
42
40
  import { Session } from "./session.js";
43
41
  import fs from "fs";
@@ -50,6 +48,9 @@ import { dirname } from "path";
50
48
  import { fileURLToPath } from "url";
51
49
  import { Level } from "level";
52
50
  import { MemoryLevel } from "memory-level";
51
+ import { Trust } from "./trust.js";
52
+ import { getBody, verifyRequest } from "./signes-request.js";
53
+ import { cli } from "./cli.js";
53
54
 
54
55
  const __dirname = dirname(fileURLToPath(import.meta.url));
55
56
 
@@ -62,62 +63,21 @@ export const stopAndWait = (server: http.Server) => {
62
63
  return waitFor(() => closed);
63
64
  };
64
65
 
65
- export const createPassword = async (
66
- configDirectory: string,
67
- password?: string
68
- ): Promise<string> => {
69
- const configDir = configDirectory ?? (await getHomeConfigDir());
70
- const credentialsPath = await getCredentialsPath(configDir);
71
- if (!password && (await checkExistPath(credentialsPath))) {
72
- throw new Error(
73
- "Config path for credentials: " + credentialsPath + ", already exist"
74
- );
75
- }
76
- console.log(`Creating config folder ${configDir}`);
77
-
78
- fs.mkdirSync(configDir, { recursive: true });
79
- await waitFor(() => fs.existsSync(configDir));
80
-
81
- console.log(`Created config folder ${configDir}`);
82
-
83
- password = password || uuid();
84
- fs.writeFileSync(
85
- credentialsPath,
86
- JSON.stringify({ username: "admin", password })
87
- );
88
- console.log(`Created credentials at ${credentialsPath}`);
89
- return password!;
90
- };
91
-
92
- export const loadOrCreatePassword = async (
93
- configDirectory: string,
94
- password?: string
95
- ): Promise<string> => {
96
- if (!password) {
97
- try {
98
- return await loadPassword(configDirectory);
99
- } catch (error) {
100
- if (error instanceof NotFoundError) {
101
- return createPassword(configDirectory, password);
102
- }
103
- throw error;
104
- }
105
- } else {
106
- return createPassword(configDirectory, password);
107
- }
108
- };
109
66
  export const startServerWithNode = async (properties: {
110
- directory?: string;
67
+ directory: string;
111
68
  domain?: string;
112
69
  bootstrap?: boolean;
113
70
  newSession?: boolean;
114
- password?: string;
115
71
  ports?: {
116
72
  node: number;
117
73
  api: number;
118
74
  };
119
75
  restart?: () => void;
120
76
  }) => {
77
+ if (!fs.existsSync(properties.directory)) {
78
+ fs.mkdirSync(properties.directory, { recursive: true });
79
+ }
80
+ const keypair = await getKeypair(properties.directory);
121
81
  const peer = await create({
122
82
  directory:
123
83
  properties.directory != null
@@ -125,6 +85,7 @@ export const startServerWithNode = async (properties: {
125
85
  : undefined,
126
86
  domain: properties.domain,
127
87
  listenPort: properties.ports?.node,
88
+ peerId: await keypair.toPeerId(),
128
89
  });
129
90
 
130
91
  if (properties.bootstrap) {
@@ -134,6 +95,7 @@ export const startServerWithNode = async (properties: {
134
95
  properties.directory != null
135
96
  ? path.join(properties.directory, "session")
136
97
  : undefined;
98
+
137
99
  const session = new Session(
138
100
  sessionDirectory
139
101
  ? new Level<string, Uint8Array>(sessionDirectory, {
@@ -165,7 +127,6 @@ export const startServerWithNode = async (properties: {
165
127
  ? path.join(properties.directory, "server")
166
128
  : undefined || getHomeConfigDir(),
167
129
  session,
168
- password: properties.password,
169
130
  });
170
131
  const printNodeInfo = async () => {
171
132
  console.log("Starting node with address(es): ");
@@ -206,11 +167,7 @@ export const startServerWithNode = async (properties: {
206
167
  return { server, node: peer };
207
168
  };
208
169
 
209
- const getProgramFromPath = (
210
- client: Peerbit,
211
- req: http.IncomingMessage,
212
- pathIndex: number
213
- ): Program | undefined => {
170
+ const getPathValue = (req: http.IncomingMessage, pathIndex: number): string => {
214
171
  if (!req.url) {
215
172
  throw new Error("Missing url");
216
173
  }
@@ -222,7 +179,7 @@ const getProgramFromPath = (
222
179
  throw new Error("Invalid path");
223
180
  }
224
181
  const address = decodeURIComponent(path[pathIndex]);
225
- return client.handler?.items.get(address);
182
+ return address;
226
183
  };
227
184
  function findPeerbitProgramFolder(inputDirectory: string): string | null {
228
185
  let currentDir = path.resolve(inputDirectory);
@@ -254,18 +211,18 @@ function findPeerbitProgramFolder(inputDirectory: string): string | null {
254
211
 
255
212
  export const startApiServer = async (
256
213
  client: ProgramClient,
257
- options: {
214
+ properties: {
258
215
  configDirectory: string;
259
216
  session?: Session;
260
217
  port?: number;
261
- password?: string;
262
218
  }
263
219
  ): Promise<http.Server> => {
264
- const port = options?.port ?? LOCAL_PORT;
265
- const password = await loadOrCreatePassword(
266
- options.configDirectory,
267
- options?.password
268
- );
220
+ const port = properties?.port ?? LOCAL_PORT;
221
+ if (!fs.existsSync(properties.configDirectory)) {
222
+ fs.mkdirSync(properties.configDirectory, { recursive: true });
223
+ }
224
+
225
+ const trust = new Trust(getTrustPath(properties.configDirectory));
269
226
 
270
227
  const restart = async () => {
271
228
  await client.stop();
@@ -285,43 +242,27 @@ export const startApiServer = async (
285
242
  gid: process.getgid!(),
286
243
  }
287
244
  );
288
-
289
- /* process.on("exit", async () => {
290
- child.kill("SIGINT")
291
- });
292
- process.on("SIGINT", async () => {
293
- child.kill("SIGINT")
294
- }); */
295
245
  process.exit(0);
296
246
  };
247
+ if (!client.peerId.equals(await client.identity.publicKey.toPeerId())) {
248
+ throw new Error("Expecting node identity to equal peerId");
249
+ }
297
250
 
298
- const adminACL = (req: http.IncomingMessage): boolean => {
299
- const auth = req.headers["authorization"];
300
- if (!auth?.startsWith("Basic ")) {
301
- return false;
302
- }
303
- const credentials = auth?.substring("Basic ".length);
304
- const username = credentials.split(":")[0];
305
- if (username !== "admin") {
306
- return false;
251
+ const getVerifiedBody = async (req: http.IncomingMessage) => {
252
+ const body = await getBody(req);
253
+ const result = await verifyRequest(
254
+ req.headers,
255
+ req.method!,
256
+ req.url!,
257
+ body
258
+ );
259
+ if (result.equals(client.identity.publicKey)) {
260
+ return body;
307
261
  }
308
- if (password !== credentials.substring(username.length + 1)) {
309
- return false;
262
+ if (trust.isTrusted(result.hashcode())) {
263
+ return body;
310
264
  }
311
- return true;
312
- };
313
-
314
- const getBody = (
315
- req: http.IncomingMessage,
316
- callback: (body: string) => Promise<void> | void
317
- ) => {
318
- let body = "";
319
- req.on("data", function (d) {
320
- body += d;
321
- });
322
- req.on("end", function () {
323
- callback(body);
324
- });
265
+ throw new Error("Not trusted");
325
266
  };
326
267
 
327
268
  const e404 = "404";
@@ -339,15 +280,20 @@ export const startApiServer = async (
339
280
 
340
281
  try {
341
282
  if (req.url) {
342
- if (
343
- !req.url.startsWith(PEER_ID_PATH) &&
344
- !req.url.startsWith(ADDRESS_PATH) &&
345
- !(await adminACL(req))
346
- ) {
283
+ let body: string;
284
+ try {
285
+ body =
286
+ req.url.startsWith(PEER_ID_PATH) ||
287
+ req.url.startsWith(ADDRESS_PATH)
288
+ ? await getBody(req)
289
+ : await getVerifiedBody(req);
290
+ } catch (error: any) {
347
291
  res.writeHead(401);
348
- res.end("Not authorized");
292
+ res.end("Not authorized: " + error.toString());
349
293
  return;
350
- } else if (req.url.startsWith(PROGRAMS_PATH)) {
294
+ }
295
+
296
+ if (req.url.startsWith(PROGRAMS_PATH)) {
351
297
  if (client instanceof Peerbit === false) {
352
298
  res.writeHead(400);
353
299
  res.end("Server node is not running a native client");
@@ -380,7 +326,9 @@ export const startApiServer = async (
380
326
  switch (req.method) {
381
327
  case "HEAD":
382
328
  try {
383
- const program = getProgramFromPath(client as Peerbit, req, 1);
329
+ const program = (client as Peerbit).handler?.items.get(
330
+ getPathValue(req, 1)
331
+ );
384
332
  if (program) {
385
333
  res.writeHead(200);
386
334
  res.end();
@@ -399,7 +347,9 @@ export const startApiServer = async (
399
347
  const url = new URL(req.url, "http://localhost:" + 1234);
400
348
  const queryData = url.searchParams.get("delete");
401
349
 
402
- const program = getProgramFromPath(client as Peerbit, req, 1);
350
+ const program = (client as Peerbit).handler?.items.get(
351
+ getPathValue(req, 1)
352
+ );
403
353
  if (program) {
404
354
  let closed = false;
405
355
  if (queryData === "true") {
@@ -408,7 +358,9 @@ export const startApiServer = async (
408
358
  closed = await program.close();
409
359
  }
410
360
  if (closed) {
411
- await options?.session?.programs.remove(program.address);
361
+ await properties?.session?.programs.remove(
362
+ program.address
363
+ );
412
364
  }
413
365
 
414
366
  res.writeHead(200);
@@ -423,56 +375,54 @@ export const startApiServer = async (
423
375
  }
424
376
  break;
425
377
  case "PUT":
426
- getBody(req, (body) => {
427
- try {
428
- const startArguments: StartProgram = JSON.parse(body);
429
-
430
- let program: Program;
431
- if ((startArguments as StartByVariant).variant) {
432
- const P = getProgramFromVariant(
433
- (startArguments as StartByVariant).variant
434
- );
435
- if (!P) {
436
- res.writeHead(400);
437
- res.end(
438
- "Missing program with variant: " +
439
- (startArguments as StartByVariant).variant
440
- );
441
- return;
442
- }
443
- program = new P();
444
- } else {
445
- program = deserialize(
446
- fromBase64((startArguments as StartByBase64).base64),
447
- Program
378
+ try {
379
+ const startArguments: StartProgram = JSON.parse(body);
380
+
381
+ let program: Program;
382
+ if ((startArguments as StartByVariant).variant) {
383
+ const P = getProgramFromVariant(
384
+ (startArguments as StartByVariant).variant
385
+ );
386
+ if (!P) {
387
+ res.writeHead(400);
388
+ res.end(
389
+ "Missing program with variant: " +
390
+ (startArguments as StartByVariant).variant
448
391
  );
392
+ return;
449
393
  }
450
- client
451
- .open(program) // TODO all users to pass args
452
- .then(async (program) => {
453
- // TODO what if this is a reopen?
454
- console.log(
455
- "OPEN ADDRESS",
456
- program.address,
457
- (client as Peerbit).directory,
458
- await client.services.blocks.has(program.address)
459
- );
460
- await options?.session?.programs.add(
461
- program.address,
462
- new Uint8Array()
463
- );
464
- res.writeHead(200);
465
- res.end(program.address.toString());
466
- })
467
- .catch((error) => {
468
- res.writeHead(400);
469
- res.end("Failed to open program: " + error.toString());
470
- });
471
- } catch (error: any) {
472
- res.writeHead(400);
473
- res.end(error.toString());
394
+ program = new P();
395
+ } else {
396
+ program = deserialize(
397
+ fromBase64((startArguments as StartByBase64).base64),
398
+ Program
399
+ );
474
400
  }
475
- });
401
+ client
402
+ .open(program) // TODO all users to pass args
403
+ .then(async (program) => {
404
+ // TODO what if this is a reopen?
405
+ console.log(
406
+ "OPEN ADDRESS",
407
+ program.address,
408
+ (client as Peerbit).directory,
409
+ await client.services.blocks.has(program.address)
410
+ );
411
+ await properties?.session?.programs.add(
412
+ program.address,
413
+ new Uint8Array()
414
+ );
415
+ res.writeHead(200);
416
+ res.end(program.address.toString());
417
+ })
418
+ .catch((error) => {
419
+ res.writeHead(400);
420
+ res.end("Failed to open program: " + error.toString());
421
+ });
422
+ } catch (error: any) {
423
+ res.writeHead(400);
424
+ res.end(error.toString());
425
+ }
476
426
  break;
477
427
 
478
428
  default:
@@ -481,105 +431,101 @@ export const startApiServer = async (
481
431
  }
482
432
  } else if (req.url.startsWith(INSTALL_PATH)) {
483
433
  switch (req.method) {
484
- case "PUT":
485
- getBody(req, async (body) => {
486
- const installArgs: InstallDependency = JSON.parse(body);
487
-
488
- const packageName = installArgs.name; // @abc/123
489
- let installName = installArgs.name; // abc123.tgz or @abc/123 (npm package name)
490
- let clear: (() => void) | undefined;
491
- if (installArgs.type === "tgz") {
492
- const binary = fromBase64(installArgs.base64);
493
- const tempFile = tmp.fileSync({
494
- name:
495
- base58btc.encode(Buffer.from(installName)) +
496
- uuid() +
497
- ".tgz",
498
- });
499
- fs.writeFileSync(tempFile.fd, binary);
500
- clear = () => tempFile.removeCallback();
501
- installName = tempFile.name;
502
- } else {
503
- clear = undefined;
504
- }
505
-
506
- if (!installName || installName.length === 0) {
507
- res.writeHead(400);
508
- res.end("Invalid package: " + packageName);
509
- } else {
510
- try {
511
- // TODO do this without sudo. i.e. for servers provide arguments so that this app folder is writeable by default by the user
512
- const installDir =
513
- process.env.PEERBIT_MODULES_PATH ||
514
- findPeerbitProgramFolder(__dirname);
515
- let permission = "";
516
- if (!installDir) {
517
- res.writeHead(400);
518
- res.end("Missing installation directory");
519
- return;
520
- }
521
- try {
522
- fs.accessSync(installDir, fs.constants.W_OK);
523
- } catch (error) {
524
- permission = "sudo";
525
- }
434
+ case "PUT": {
435
+ const installArgs: InstallDependency = JSON.parse(body);
436
+
437
+ const packageName = installArgs.name; // @abc/123
438
+ let installName = installArgs.name; // abc123.tgz or @abc/123 (npm package name)
439
+ let clear: (() => void) | undefined;
440
+ if (installArgs.type === "tgz") {
441
+ const binary = fromBase64(installArgs.base64);
442
+ const tempFile = tmp.fileSync({
443
+ name:
444
+ base58btc.encode(Buffer.from(installName)) +
445
+ uuid() +
446
+ ".tgz",
447
+ });
448
+ fs.writeFileSync(tempFile.fd, binary);
449
+ clear = () => tempFile.removeCallback();
450
+ installName = tempFile.name;
451
+ } else {
452
+ clear = undefined;
453
+ }
526
454
 
527
- console.log("Installing package: " + installName);
528
- execSync(
529
- `${permission} npm install ${installName} --prefix ${installDir} --no-save --no-package-lock`
530
- ); // TODO omit=dev ? but this makes breaks the tests after running once?
531
- } catch (error: any) {
455
+ if (!installName || installName.length === 0) {
456
+ res.writeHead(400);
457
+ res.end("Invalid package: " + packageName);
458
+ } else {
459
+ try {
460
+ // TODO do this without sudo. i.e. for servers provide arguments so that this app folder is writeable by default by the user
461
+ const installDir =
462
+ process.env.PEERBIT_MODULES_PATH ||
463
+ findPeerbitProgramFolder(__dirname);
464
+ let permission = "";
465
+ if (!installDir) {
532
466
  res.writeHead(400);
533
- res.end(
534
- "Failed to install library: " +
535
- packageName +
536
- ". " +
537
- error.toString()
538
- );
539
- clear?.();
467
+ res.end("Missing installation directory");
540
468
  return;
541
469
  }
542
-
543
470
  try {
544
- const programsPre = new Set(
545
- getProgramFromVariants().map(
546
- (x) => getSchema(x).variant
547
- )
548
- );
471
+ fs.accessSync(installDir, fs.constants.W_OK);
472
+ } catch (error) {
473
+ permission = "sudo";
474
+ }
549
475
 
550
- await import(
551
- /* webpackIgnore: true */ /* @vite-ignore */ packageName
552
- );
553
- await options?.session?.imports.add(
554
- packageName,
555
- new Uint8Array()
556
- );
557
- const programsPost = getProgramFromVariants()?.map((x) =>
558
- getSchema(x)
559
- );
560
- const newPrograms: { variant: string }[] = [];
561
- for (const p of programsPost) {
562
- if (!programsPre.has(p.variant)) {
563
- newPrograms.push(p as { variant: string });
564
- }
565
- }
476
+ console.log("Installing package: " + installName);
477
+ execSync(
478
+ `${permission} npm install ${installName} --prefix ${installDir} --no-save --no-package-lock`
479
+ ); // TODO omit=dev ? but this makes breaks the tests after running once?
480
+ } catch (error: any) {
481
+ res.writeHead(400);
482
+ res.end(
483
+ "Failed to install library: " +
484
+ packageName +
485
+ ". " +
486
+ error.toString()
487
+ );
488
+ clear?.();
489
+ return;
490
+ }
566
491
 
567
- res.writeHead(200);
568
- res.end(
569
- JSON.stringify(newPrograms.map((x) => x.variant))
570
- );
571
- } catch (e: any) {
572
- res.writeHead(400);
573
- res.end(e.message.toString?.());
574
- clear?.();
492
+ try {
493
+ const programsPre = new Set(
494
+ getProgramFromVariants().map((x) => getSchema(x).variant)
495
+ );
496
+
497
+ await import(
498
+ /* webpackIgnore: true */ /* @vite-ignore */ packageName
499
+ );
500
+ await properties?.session?.imports.add(
501
+ packageName,
502
+ new Uint8Array()
503
+ );
504
+ const programsPost = getProgramFromVariants()?.map((x) =>
505
+ getSchema(x)
506
+ );
507
+ const newPrograms: { variant: string }[] = [];
508
+ for (const p of programsPost) {
509
+ if (!programsPre.has(p.variant)) {
510
+ newPrograms.push(p as { variant: string });
511
+ }
575
512
  }
513
+
514
+ res.writeHead(200);
515
+ res.end(JSON.stringify(newPrograms.map((x) => x.variant)));
516
+ } catch (e: any) {
517
+ res.writeHead(400);
518
+ res.end(e.message.toString?.());
519
+ clear?.();
576
520
  }
577
- });
521
+ }
578
522
  break;
523
+ }
579
524
 
580
- default:
525
+ default: {
581
526
  r404();
582
527
  break;
528
+ }
583
529
  }
584
530
  } else if (req.url.startsWith(BOOTSTRAP_PATH)) {
585
531
  switch (req.method) {
@@ -598,6 +544,24 @@ export const startApiServer = async (
598
544
  r404();
599
545
  break;
600
546
  }
547
+ } else if (req.url.startsWith(TRUST_PATH)) {
548
+ switch (req.method) {
549
+ case "PUT": {
550
+ trust.add(getPathValue(req, 1));
551
+ res.writeHead(200);
552
+ res.end();
553
+ break;
554
+ }
555
+ case "DELETE": {
556
+ const removed = trust.remove(getPathValue(req, 1));
557
+ res.writeHead(200);
558
+ res.end(removed);
559
+ break;
560
+ }
561
+ default:
562
+ r404();
563
+ break;
564
+ }
601
565
  } else if (req.url.startsWith(RESTART_PATH)) {
602
566
  switch (req.method) {
603
567
  case "POST":
@@ -0,0 +1,84 @@
1
+ import {
2
+ Identity,
3
+ PublicSignKey,
4
+ SignatureWithKey,
5
+ fromBase64,
6
+ toBase64,
7
+ verify,
8
+ Ed25519PublicKey,
9
+ } from "@peerbit/crypto";
10
+ import { deserialize, serialize, BinaryWriter } from "@dao-xyz/borsh";
11
+ import http from "http";
12
+
13
+ const SIGNATURE_KEY = "X-Peerbit-Signature";
14
+ const SIGNATURE_TIME_KEY = "X-Peerbit-Signature-Time";
15
+
16
+ export const signRequest = async (
17
+ headers: Record<string, string>,
18
+ method: string,
19
+ path: string,
20
+ data: string | undefined,
21
+ keypair: Identity<Ed25519PublicKey>
22
+ ) => {
23
+ const sigTimestamp = Math.round(new Date().getTime() / 1000).toString();
24
+ const write = new BinaryWriter();
25
+ if (!method) {
26
+ throw new Error("Expecting method");
27
+ }
28
+ if (!path) {
29
+ throw new Error("Expecting path");
30
+ }
31
+
32
+ write.string(
33
+ method.toLowerCase() + path.toLowerCase() + sigTimestamp + (data || "")
34
+ );
35
+ const signature = await keypair.sign(write.finalize());
36
+ headers[SIGNATURE_TIME_KEY] = sigTimestamp;
37
+ headers[SIGNATURE_KEY] = toBase64(serialize(signature));
38
+ };
39
+
40
+ export const getBody = (req: http.IncomingMessage): Promise<string> => {
41
+ return new Promise((resolve, reject) => {
42
+ let body = "";
43
+ req.on("data", (d) => {
44
+ body += d;
45
+ });
46
+ req.on("end", () => {
47
+ resolve(body);
48
+ });
49
+ req.on("error", (e) => reject(e));
50
+ });
51
+ };
52
+
53
+ export const verifyRequest = async (
54
+ headers: Record<string, string | string[] | undefined>,
55
+ method: string,
56
+ path: string,
57
+ body = ""
58
+ ): Promise<PublicSignKey> => {
59
+ const timestamp =
60
+ headers[SIGNATURE_TIME_KEY] || headers[SIGNATURE_TIME_KEY.toLowerCase()];
61
+ if (typeof timestamp !== "string") {
62
+ throw new Error("Unexpected timestamp type: " + typeof timestamp);
63
+ }
64
+
65
+ const write = new BinaryWriter();
66
+ if (!method) {
67
+ throw new Error("Expecting method");
68
+ }
69
+ if (!path) {
70
+ throw new Error("Expecting path");
71
+ }
72
+
73
+ write.string(method.toLowerCase() + path.toLowerCase() + timestamp + body);
74
+ const signature =
75
+ headers[SIGNATURE_KEY] || headers[SIGNATURE_KEY.toLowerCase()];
76
+ if (typeof signature !== "string") {
77
+ throw new Error("Unexpected signature type: " + typeof signature);
78
+ }
79
+ const signatureWithKey = deserialize(fromBase64(signature), SignatureWithKey);
80
+ if (await verify(signatureWithKey, write.finalize())) {
81
+ return signatureWithKey.publicKey;
82
+ }
83
+ throw new Error("Invalid signature");
84
+ };