@bitsocial/bitsocial-cli 0.19.63 → 0.19.65

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -304,6 +304,7 @@ $ bitsocial community edit mysub.bso '--roles["author-address.bso"]' null
304
304
  * [`bitsocial community create`](#bitsocial-community-create)
305
305
  * [`bitsocial community delete ADDRESSES`](#bitsocial-community-delete-addresses)
306
306
  * [`bitsocial community edit ADDRESS`](#bitsocial-community-edit-address)
307
+ * [`bitsocial community export [ADDRESS]`](#bitsocial-community-export-address)
307
308
  * [`bitsocial community get [ADDRESS]`](#bitsocial-community-get-address)
308
309
  * [`bitsocial community list`](#bitsocial-community-list)
309
310
  * [`bitsocial community start ADDRESSES`](#bitsocial-community-start-addresses)
@@ -344,7 +345,7 @@ EXAMPLES
344
345
  $ bitsocial challenge install ./my-local-challenge
345
346
  ```
346
347
 
347
- _See code: [src/cli/commands/challenge/install.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.63/src/cli/commands/challenge/install.ts)_
348
+ _See code: [src/cli/commands/challenge/install.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.65/src/cli/commands/challenge/install.ts)_
348
349
 
349
350
  ## `bitsocial challenge list`
350
351
 
@@ -367,7 +368,7 @@ EXAMPLES
367
368
  $ bitsocial challenge list -q
368
369
  ```
369
370
 
370
- _See code: [src/cli/commands/challenge/list.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.63/src/cli/commands/challenge/list.ts)_
371
+ _See code: [src/cli/commands/challenge/list.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.65/src/cli/commands/challenge/list.ts)_
371
372
 
372
373
  ## `bitsocial challenge remove NAME`
373
374
 
@@ -392,7 +393,7 @@ EXAMPLES
392
393
  $ bitsocial challenge remove @scope/my-challenge
393
394
  ```
394
395
 
395
- _See code: [src/cli/commands/challenge/remove.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.63/src/cli/commands/challenge/remove.ts)_
396
+ _See code: [src/cli/commands/challenge/remove.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.65/src/cli/commands/challenge/remove.ts)_
396
397
 
397
398
  ## `bitsocial community create`
398
399
 
@@ -422,7 +423,7 @@ EXAMPLES
422
423
  $ bitsocial community create --jsonFile ./create-options.json
423
424
  ```
424
425
 
425
- _See code: [src/cli/commands/community/create.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.63/src/cli/commands/community/create.ts)_
426
+ _See code: [src/cli/commands/community/create.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.65/src/cli/commands/community/create.ts)_
426
427
 
427
428
  ## `bitsocial community delete ADDRESSES`
428
429
 
@@ -447,7 +448,7 @@ EXAMPLES
447
448
  $ bitsocial community delete 12D3KooWG3XbzoVyAE6Y9vHZKF64Yuuu4TjdgQKedk14iYmTEPWu
448
449
  ```
449
450
 
450
- _See code: [src/cli/commands/community/delete.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.63/src/cli/commands/community/delete.ts)_
451
+ _See code: [src/cli/commands/community/delete.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.65/src/cli/commands/community/delete.ts)_
451
452
 
452
453
  ## `bitsocial community edit ADDRESS`
453
454
 
@@ -517,7 +518,48 @@ EXAMPLES
517
518
  $ bitsocial community edit bitsocial.bso --jsonFile ./edit-options.json
518
519
  ```
519
520
 
520
- _See code: [src/cli/commands/community/edit.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.63/src/cli/commands/community/edit.ts)_
521
+ _See code: [src/cli/commands/community/edit.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.65/src/cli/commands/community/edit.ts)_
522
+
523
+ ## `bitsocial community export [ADDRESS]`
524
+
525
+ Export a local community to a SQLite snapshot file. The export runs on the RPC server (daemon); once finished the snapshot is downloaded and its sha256 checksum is verified. Pass --includePrivateKey to produce a restorable backup that keeps the community's address.
526
+
527
+ ```
528
+ USAGE
529
+ $ bitsocial community export [ADDRESS] --pkcRpcUrl <value> [--name <value>] [--publicKey <value>] [-o <value>]
530
+ [--includePrivateKey] [--force] [-q]
531
+
532
+ ARGUMENTS
533
+ [ADDRESS] Address of the community to export
534
+
535
+ FLAGS
536
+ -o, --path=<value> Destination file for the downloaded snapshot (default:
537
+ <dataPath>/exports/<address>_<datetime>.sqlite)
538
+ -q, --quiet Suppress progress output; only print the path of the downloaded snapshot
539
+ --force Overwrite the destination file if it already exists
540
+ --includePrivateKey Ask the RPC server to include the community signer's private key in the export. Required for
541
+ a restorable backup that keeps the same community address. The daemon may refuse (see
542
+ `bitsocial daemon --no-allowPrivateKeyExport`)
543
+ --name=<value> Name of the community to export
544
+ --pkcRpcUrl=<value> (required) [default: ws://localhost:9138/] URL to PKC RPC
545
+ --publicKey=<value> Public key of the community to export
546
+
547
+ DESCRIPTION
548
+ Export a local community to a SQLite snapshot file. The export runs on the RPC server (daemon); once finished the
549
+ snapshot is downloaded and its sha256 checksum is verified. Pass --includePrivateKey to produce a restorable backup
550
+ that keeps the community's address.
551
+
552
+ EXAMPLES
553
+ $ bitsocial community export plebmusic.bso
554
+
555
+ $ bitsocial community export plebmusic.bso --includePrivateKey -o ./backups/plebmusic.sqlite
556
+
557
+ $ bitsocial community export --name my-community
558
+
559
+ $ bitsocial community export --publicKey 12D3KooWG3XbzoVyAE6Y9vHZKF64Yuuu4TjdgQKedk14iYmTEPWu
560
+ ```
561
+
562
+ _See code: [src/cli/commands/community/export.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.65/src/cli/commands/community/export.ts)_
521
563
 
522
564
  ## `bitsocial community get [ADDRESS]`
523
565
 
@@ -548,7 +590,7 @@ EXAMPLES
548
590
  $ bitsocial community get --publicKey 12D3KooWG3XbzoVyAE6Y9vHZKF64Yuuu4TjdgQKedk14iYmTEPWu
549
591
  ```
550
592
 
551
- _See code: [src/cli/commands/community/get.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.63/src/cli/commands/community/get.ts)_
593
+ _See code: [src/cli/commands/community/get.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.65/src/cli/commands/community/get.ts)_
552
594
 
553
595
  ## `bitsocial community list`
554
596
 
@@ -571,7 +613,7 @@ EXAMPLES
571
613
  $ bitsocial community list
572
614
  ```
573
615
 
574
- _See code: [src/cli/commands/community/list.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.63/src/cli/commands/community/list.ts)_
616
+ _See code: [src/cli/commands/community/list.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.65/src/cli/commands/community/list.ts)_
575
617
 
576
618
  ## `bitsocial community start ADDRESSES`
577
619
 
@@ -605,7 +647,7 @@ EXAMPLES
605
647
  $ bitsocial community start $(bitsocial community list -q) --concurrency 1
606
648
  ```
607
649
 
608
- _See code: [src/cli/commands/community/start.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.63/src/cli/commands/community/start.ts)_
650
+ _See code: [src/cli/commands/community/start.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.65/src/cli/commands/community/start.ts)_
609
651
 
610
652
  ## `bitsocial community stop ADDRESSES`
611
653
 
@@ -630,7 +672,7 @@ EXAMPLES
630
672
  $ bitsocial community stop Qmb99crTbSUfKXamXwZBe829Vf6w5w5TktPkb6WstC9RFW
631
673
  ```
632
674
 
633
- _See code: [src/cli/commands/community/stop.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.63/src/cli/commands/community/stop.ts)_
675
+ _See code: [src/cli/commands/community/stop.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.65/src/cli/commands/community/stop.ts)_
634
676
 
635
677
  ## `bitsocial daemon`
636
678
 
@@ -639,8 +681,12 @@ Run a network-connected Bitsocial node. Once the daemon is running you can creat
639
681
  ```
640
682
  USAGE
641
683
  $ bitsocial daemon --pkcRpcUrl <value> --logPath <value> [--chainProviderUrls <value>...]
684
+ [--allowPrivateKeyExport]
642
685
 
643
686
  FLAGS
687
+ --[no-]allowPrivateKeyExport Allow RPC clients to request community exports that include the community signer's
688
+ private key (`bitsocial community export --includePrivateKey`). Disable with
689
+ --no-allowPrivateKeyExport when exposing the RPC to untrusted clients
644
690
  --chainProviderUrls=<value>... [default:
645
691
  https://eth.drpc.org,https://ethereum.publicnode.com,https://ethereum-rpc.publicnode.c
646
692
  om,https://rpc.mevblocker.io,https://1rpc.io/eth,https://eth-pokt.nodies.app] RPC
@@ -669,9 +715,11 @@ EXAMPLES
669
715
  $ bitsocial daemon --pkcOptions.kuboRpcClientsOptions[0] https://remoteipfsnode.com
670
716
 
671
717
  $ bitsocial daemon --chainProviderUrls https://mainnet.infura.io/v3/YOUR_KEY
718
+
719
+ $ bitsocial daemon --no-allowPrivateKeyExport
672
720
  ```
673
721
 
674
- _See code: [src/cli/commands/daemon.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.63/src/cli/commands/daemon.ts)_
722
+ _See code: [src/cli/commands/daemon.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.65/src/cli/commands/daemon.ts)_
675
723
 
676
724
  ## `bitsocial help [COMMAND]`
677
725
 
@@ -737,7 +785,7 @@ EXAMPLES
737
785
  $ bitsocial logs --stdout -f
738
786
  ```
739
787
 
740
- _See code: [src/cli/commands/logs.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.63/src/cli/commands/logs.ts)_
788
+ _See code: [src/cli/commands/logs.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.65/src/cli/commands/logs.ts)_
741
789
 
742
790
  ## `bitsocial update check`
743
791
 
@@ -754,7 +802,7 @@ EXAMPLES
754
802
  $ bitsocial update check
755
803
  ```
756
804
 
757
- _See code: [src/cli/commands/update/check.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.63/src/cli/commands/update/check.ts)_
805
+ _See code: [src/cli/commands/update/check.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.65/src/cli/commands/update/check.ts)_
758
806
 
759
807
  ## `bitsocial update install [VERSION]`
760
808
 
@@ -786,7 +834,7 @@ EXAMPLES
786
834
  $ bitsocial update install --no-restart-daemons
787
835
  ```
788
836
 
789
- _See code: [src/cli/commands/update/install.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.63/src/cli/commands/update/install.ts)_
837
+ _See code: [src/cli/commands/update/install.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.65/src/cli/commands/update/install.ts)_
790
838
 
791
839
  ## `bitsocial update versions`
792
840
 
@@ -808,7 +856,7 @@ EXAMPLES
808
856
  $ bitsocial update versions --limit 5
809
857
  ```
810
858
 
811
- _See code: [src/cli/commands/update/versions.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.63/src/cli/commands/update/versions.ts)_
859
+ _See code: [src/cli/commands/update/versions.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.65/src/cli/commands/update/versions.ts)_
812
860
  <!-- commandsstop -->
813
861
 
814
862
  ## Contribution
@@ -1 +1,8 @@
1
- export declare function printBanner(): void;
1
+ interface RenderBannerOptions {
2
+ env?: Record<string, string | undefined>;
3
+ forceColor?: boolean;
4
+ stdoutIsTTY?: boolean;
5
+ }
6
+ export declare function renderBanner(options?: RenderBannerOptions): string;
7
+ export declare function printBanner(options?: RenderBannerOptions): void;
8
+ export {};
@@ -5,8 +5,8 @@
5
5
  //
6
6
  // COLORS is the paint map. Each character corresponds 1:1 to the character
7
7
  // at the same position in SHAPE:
8
- // B = blue (#1a4fd0) — the sphere
9
- // S = silver (#e5e7eb) — the rings and the "Bitsocial" text
8
+ // B = blue accent — the sphere
9
+ // S = default foreground — the rings and the "Bitsocial" text
10
10
  // . = no color (pass the glyph through as-is; use this for spaces)
11
11
  //
12
12
  // To retouch the art, find a glyph in SHAPE, then flip the character at the
@@ -15,7 +15,8 @@
15
15
  //
16
16
  // Both grids MUST have the same number of rows. Each row in COLORS must be at
17
17
  // least as wide as the corresponding SHAPE row (extra chars are ignored).
18
- // Palette sourced from bitsocialnet/bitsocial-web/about/tailwind.config.ts.
18
+ // Use the terminal's default foreground for the wordmark/rings so the banner
19
+ // stays readable on both light and dark terminal themes.
19
20
  const SHAPE = [
20
21
  " ⢀⣴⣿⣿⣦⡀ ",
21
22
  " ⣾⣿⠁⠈⣿⣷⡀ ",
@@ -56,39 +57,45 @@ const COLORS = [
56
57
  "...............SSSSSSSS......................................................................................",
57
58
  "................SSSSSS......................................................................................."
58
59
  ];
59
- const BLUE = "\x1b[38;2;26;79;208m";
60
- const SILVER = "\x1b[38;2;229;231;235m";
61
- const RESET = "\x1b[0m";
60
+ const BLUE = "\x1b[94m";
61
+ const DEFAULT_FOREGROUND = "\x1b[39m";
62
62
  function paint(shape, colors) {
63
63
  let out = "";
64
- let current = ".";
64
+ let blueActive = false;
65
65
  for (let i = 0; i < shape.length; i++) {
66
66
  const glyph = shape[i];
67
67
  const want = colors[i] ?? ".";
68
- if (want !== current) {
69
- if (current !== ".")
70
- out += RESET;
71
- if (want === "B")
72
- out += BLUE;
73
- else if (want === "S")
74
- out += SILVER;
75
- current = want;
68
+ const wantBlue = want === "B";
69
+ if (wantBlue !== blueActive) {
70
+ out += wantBlue ? BLUE : DEFAULT_FOREGROUND;
71
+ blueActive = wantBlue;
76
72
  }
77
73
  out += glyph;
78
74
  }
79
- if (current !== ".")
80
- out += RESET;
75
+ if (blueActive)
76
+ out += DEFAULT_FOREGROUND;
81
77
  return out;
82
78
  }
83
- function supportsColor() {
84
- if (process.env["NO_COLOR"])
79
+ function envForcesColor(value) {
80
+ if (value === undefined)
85
81
  return false;
86
- if (process.env["FORCE_COLOR"])
82
+ return value !== "0" && value.toLowerCase() !== "false";
83
+ }
84
+ function supportsColor(options = {}) {
85
+ const env = options.env ?? process.env;
86
+ if (env["NO_COLOR"] !== undefined)
87
+ return false;
88
+ if (options.forceColor)
87
89
  return true;
88
- return Boolean(process.stdout.isTTY);
90
+ if (env["FORCE_COLOR"] !== undefined)
91
+ return envForcesColor(env["FORCE_COLOR"]);
92
+ return Boolean(options.stdoutIsTTY ?? process.stdout.isTTY);
89
93
  }
90
- export function printBanner() {
91
- const useColor = supportsColor();
94
+ export function renderBanner(options = {}) {
95
+ const useColor = supportsColor(options);
92
96
  const lines = SHAPE.map((row, i) => (useColor ? paint(row, COLORS[i] ?? "") : row));
93
- process.stdout.write(lines.join("\n") + "\n\n");
97
+ return lines.join("\n") + "\n\n";
98
+ }
99
+ export function printBanner(options = {}) {
100
+ process.stdout.write(renderBanner(options));
94
101
  }
@@ -0,0 +1,22 @@
1
+ import { BaseCommand } from "../../base-command.js";
2
+ export default class Export extends BaseCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static args: {
6
+ address: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
7
+ };
8
+ static flags: {
9
+ name: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ publicKey: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ path: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ includePrivateKey: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
+ force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
14
+ quiet: import("@oclif/core/interfaces").BooleanFlag<boolean>;
15
+ };
16
+ private _printProgress;
17
+ run(): Promise<void>;
18
+ /** Start the export on the RPC server and resolve with the terminal record (progress === 1). */
19
+ private _runExport;
20
+ /** Download the finished snapshot to destPath, verifying its sha256 against the export record. */
21
+ private _downloadAndVerify;
22
+ }
@@ -0,0 +1,198 @@
1
+ import { Args, Flags } from "@oclif/core";
2
+ import { BaseCommand } from "../../base-command.js";
3
+ import defaults from "../../../common-utils/defaults.js";
4
+ import { PKCLogger } from "../../../util.js";
5
+ import { createHash } from "node:crypto";
6
+ import { createWriteStream } from "node:fs";
7
+ import fs from "node:fs/promises";
8
+ import path from "node:path";
9
+ import { Readable } from "node:stream";
10
+ import { pipeline } from "node:stream/promises";
11
+ export default class Export extends BaseCommand {
12
+ static description = "Export a local community to a SQLite snapshot file. The export runs on the RPC server (daemon); once finished the snapshot is downloaded and its sha256 checksum is verified. Pass --includePrivateKey to produce a restorable backup that keeps the community's address.";
13
+ static examples = [
14
+ "bitsocial community export plebmusic.bso",
15
+ "bitsocial community export plebmusic.bso --includePrivateKey -o ./backups/plebmusic.sqlite",
16
+ "bitsocial community export --name my-community",
17
+ "bitsocial community export --publicKey 12D3KooWG3XbzoVyAE6Y9vHZKF64Yuuu4TjdgQKedk14iYmTEPWu"
18
+ ];
19
+ static args = {
20
+ address: Args.string({
21
+ name: "address",
22
+ required: false,
23
+ description: "Address of the community to export"
24
+ })
25
+ };
26
+ static flags = {
27
+ name: Flags.string({
28
+ description: "Name of the community to export"
29
+ }),
30
+ publicKey: Flags.string({
31
+ description: "Public key of the community to export"
32
+ }),
33
+ path: Flags.string({
34
+ char: "o",
35
+ description: "Destination file for the downloaded snapshot (default: <dataPath>/exports/<address>_<datetime>.sqlite)"
36
+ }),
37
+ includePrivateKey: Flags.boolean({
38
+ default: false,
39
+ description: "Ask the RPC server to include the community signer's private key in the export. Required for a restorable backup that keeps the same community address. The daemon may refuse (see `bitsocial daemon --no-allowPrivateKeyExport`)"
40
+ }),
41
+ force: Flags.boolean({
42
+ default: false,
43
+ description: "Overwrite the destination file if it already exists"
44
+ }),
45
+ quiet: Flags.boolean({
46
+ char: "q",
47
+ default: false,
48
+ description: "Suppress progress output; only print the path of the downloaded snapshot"
49
+ })
50
+ };
51
+ _printProgress(quiet, message) {
52
+ if (!quiet)
53
+ process.stderr.write(message);
54
+ }
55
+ async run() {
56
+ const { args, flags } = await this.parse(Export);
57
+ const log = PKCLogger("bitsocial-cli:commands:community:export");
58
+ log(`args: `, args);
59
+ log(`flags: `, flags);
60
+ const lookupParam = {};
61
+ if (args.address)
62
+ lookupParam.address = args.address;
63
+ if (flags.name)
64
+ lookupParam.name = flags.name;
65
+ if (flags.publicKey)
66
+ lookupParam.publicKey = flags.publicKey;
67
+ if (Object.keys(lookupParam).length === 0) {
68
+ this.error("At least one of address argument, --name, or --publicKey must be provided");
69
+ }
70
+ const pkc = await this._connectToPkcRpc(flags.pkcRpcUrl.toString());
71
+ // Cancel the in-flight export server-side on Ctrl+C. A second Ctrl+C force-exits
72
+ // (the handler is registered with `once`, so the default SIGINT behavior is restored).
73
+ const abortController = new AbortController();
74
+ const onSigint = () => {
75
+ this._printProgress(flags.quiet, "\nCancelling export... (Ctrl+C again to force exit)\n");
76
+ abortController.abort();
77
+ };
78
+ process.once("SIGINT", onSigint);
79
+ try {
80
+ const community = (await pkc.createCommunity(lookupParam));
81
+ if (typeof community.export !== "function") {
82
+ this.error(`Community is not local to the RPC server at ${flags.pkcRpcUrl}. Only communities created on this daemon can be exported`);
83
+ }
84
+ const exportableCommunity = community;
85
+ // Datetime in the filename matches the daemon log convention (ISO 8601 with ':' → '-')
86
+ // so repeated exports never collide and snapshots sort chronologically
87
+ const defaultFilename = `${exportableCommunity.address}_${new Date().toISOString().replace(/:/g, "-")}.sqlite`;
88
+ const destPath = path.resolve(flags.path ?? path.join(defaults.PKC_DATA_PATH, "exports", defaultFilename));
89
+ const destExists = await fs
90
+ .stat(destPath)
91
+ .then(() => true)
92
+ .catch(() => false);
93
+ if (destExists && !flags.force) {
94
+ this.error(`Destination file already exists: ${destPath}. Use --force to overwrite it`);
95
+ }
96
+ const finishedRecord = await this._runExport(exportableCommunity, flags.includePrivateKey, abortController.signal, flags.quiet);
97
+ log("Export finished on the RPC server", finishedRecord);
98
+ if (!finishedRecord.url) {
99
+ this.error(`Export ${finishedRecord.exportId} finished but the RPC server did not provide a download URL`);
100
+ }
101
+ // Mirrors pkc-js's rpcHttpOrigin: the ws[s]:// RPC URL with the protocol swapped to http[s]://
102
+ const parsedRpcUrl = new URL(flags.pkcRpcUrl.toString());
103
+ const expectedDownloadOrigin = `${parsedRpcUrl.protocol === "wss:" ? "https:" : "http:"}//${parsedRpcUrl.host}`;
104
+ await this._downloadAndVerify(finishedRecord, destPath, abortController.signal, flags.quiet, expectedDownloadOrigin);
105
+ this.log(destPath);
106
+ }
107
+ catch (e) {
108
+ console.error(e);
109
+ await pkc.destroy();
110
+ this.exit(1);
111
+ }
112
+ finally {
113
+ process.removeListener("SIGINT", onSigint);
114
+ }
115
+ await pkc.destroy();
116
+ }
117
+ /** Start the export on the RPC server and resolve with the terminal record (progress === 1). */
118
+ async _runExport(community, includePrivateKey, signal, quiet) {
119
+ const { exportId } = await community.export({ includePrivateKey, signal });
120
+ return new Promise((resolve, reject) => {
121
+ let lastPrintedPercent = -1;
122
+ const cleanup = () => {
123
+ community.removeListener("exportschange", checkRecords);
124
+ signal.removeEventListener("abort", onAbort);
125
+ };
126
+ // Don't wait for the server's terminal ERR_EXPORT_CANCELLED record — it never arrives if the
127
+ // daemon died or the connection dropped. pkc-js's own abort listener (registered inside
128
+ // community.export(), before this one) already dispatched cancelExport() to the server.
129
+ const onAbort = () => {
130
+ cleanup();
131
+ reject(new Error("Export cancelled"));
132
+ };
133
+ const checkRecords = (records) => {
134
+ const record = records.find((rec) => rec.exportId === exportId);
135
+ if (!record)
136
+ return;
137
+ if (record.error) {
138
+ cleanup();
139
+ reject(new Error(`Export failed (${record.error.code}): ${record.error.message}`));
140
+ }
141
+ else if (record.progress === 1) {
142
+ cleanup();
143
+ this._printProgress(quiet, `\rExporting ${community.address}: 100%\n`);
144
+ resolve(record);
145
+ }
146
+ else {
147
+ const percent = Math.floor(record.progress * 100);
148
+ if (percent !== lastPrintedPercent) {
149
+ lastPrintedPercent = percent;
150
+ this._printProgress(quiet, `\rExporting ${community.address}: ${percent}%`);
151
+ }
152
+ }
153
+ };
154
+ community.on("exportschange", checkRecords);
155
+ signal.addEventListener("abort", onAbort, { once: true });
156
+ if (signal.aborted)
157
+ return onAbort();
158
+ // The terminal notification may have arrived before the listener was attached
159
+ checkRecords(community.exports);
160
+ });
161
+ }
162
+ /** Download the finished snapshot to destPath, verifying its sha256 against the export record. */
163
+ async _downloadAndVerify(record, destPath, signal, quiet, expectedOrigin) {
164
+ // The export download contract is GET <rpc-http-origin>/exports/<exportId> — refuse anything else
165
+ // so a misconfigured/compromised RPC server can't use the CLI to fetch arbitrary URLs
166
+ const downloadUrl = new URL(record.url);
167
+ if (downloadUrl.origin !== expectedOrigin || !downloadUrl.pathname.startsWith("/exports/")) {
168
+ this.error(`Refusing to download export from unexpected URL ${record.url} (expected ${expectedOrigin}/exports/<exportId>)`);
169
+ }
170
+ this._printProgress(quiet, `Downloading snapshot from ${downloadUrl}\n`);
171
+ const response = await fetch(downloadUrl, { signal });
172
+ if (!response.ok || !response.body) {
173
+ this.error(`Failed to download export from ${record.url}: HTTP ${response.status}`);
174
+ }
175
+ await fs.mkdir(path.dirname(destPath), { recursive: true });
176
+ // Download to a .partial file so an interrupted/corrupted download never clobbers destPath
177
+ const partialPath = destPath + ".partial";
178
+ const hash = createHash("sha256");
179
+ try {
180
+ await pipeline(Readable.fromWeb(response.body), async function* (source) {
181
+ for await (const chunk of source) {
182
+ hash.update(chunk);
183
+ yield chunk;
184
+ }
185
+ }, createWriteStream(partialPath));
186
+ const downloadedSha256 = hash.digest("hex");
187
+ if (record.sha256 && downloadedSha256 !== record.sha256) {
188
+ throw new Error(`sha256 mismatch for downloaded export: expected ${record.sha256} but downloaded file hashes to ${downloadedSha256}`);
189
+ }
190
+ await fs.rename(partialPath, destPath);
191
+ }
192
+ catch (e) {
193
+ await fs.rm(partialPath, { force: true }).catch(() => { });
194
+ throw e;
195
+ }
196
+ this._printProgress(quiet, `Verified sha256 (${record.sha256}) and saved snapshot${record.size ? ` (${record.size} bytes)` : ""}\n`);
197
+ }
198
+ }
@@ -26,6 +26,7 @@ export default class Daemon extends Command {
26
26
  pkcRpcUrl: import("@oclif/core/interfaces").OptionFlag<import("url").URL, import("@oclif/core/interfaces").CustomOptions>;
27
27
  logPath: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
28
28
  chainProviderUrls: import("@oclif/core/interfaces").OptionFlag<string[], import("@oclif/core/interfaces").CustomOptions>;
29
+ allowPrivateKeyExport: import("@oclif/core/interfaces").BooleanFlag<boolean>;
29
30
  };
30
31
  static examples: string[];
31
32
  private _setupLogger;
@@ -83,6 +83,11 @@ export default class Daemon extends Command {
83
83
  description: "RPC URL(s) for .bso name resolution. Can be specified multiple times.",
84
84
  multiple: true,
85
85
  default: DEFAULT_PROVIDERS
86
+ }),
87
+ allowPrivateKeyExport: Flags.boolean({
88
+ description: "Allow RPC clients to request community exports that include the community signer's private key (`bitsocial community export --includePrivateKey`). Disable with --no-allowPrivateKeyExport when exposing the RPC to untrusted clients",
89
+ allowNo: true,
90
+ default: true
86
91
  })
87
92
  };
88
93
  static examples = [
@@ -91,6 +96,7 @@ export default class Daemon extends Command {
91
96
  "bitsocial daemon --pkcOptions.dataPath /tmp/bitsocial-datapath/",
92
97
  "bitsocial daemon --pkcOptions.kuboRpcClientsOptions[0] https://remoteipfsnode.com",
93
98
  "bitsocial daemon --chainProviderUrls https://mainnet.infura.io/v3/YOUR_KEY",
99
+ "bitsocial daemon --no-allowPrivateKeyExport",
94
100
  ];
95
101
  _setupLogger(Logger) {
96
102
  setupDebugLogger(Logger, { enableDefaultNamespace: true });
@@ -187,7 +193,8 @@ export default class Daemon extends Command {
187
193
  return { logFilePath, stdoutWrite, fileLogger };
188
194
  }
189
195
  async run() {
190
- printBanner();
196
+ // Daemon output is often viewed through Docker/systemd logs where stdout is not a TTY.
197
+ printBanner({ forceColor: true });
191
198
  // Non-blocking update check — fire-and-forget, won't delay startup
192
199
  import("../../update/npm-registry.js")
193
200
  .then(({ fetchLatestVersion }) => fetchLatestVersion().then(async (latest) => {
@@ -378,7 +385,9 @@ export default class Daemon extends Command {
378
385
  const loadedChallenges = await loadChallengesIntoPKC(mergedPkcOptions.dataPath);
379
386
  if (loadedChallenges.length > 0)
380
387
  console.log(`Loaded challenge packages: ${loadedChallenges.join(", ")}`);
381
- daemonServer = await startDaemonServer(pkcRpcUrl, ipfsGatewayEndpoint, mergedPkcOptions);
388
+ daemonServer = await startDaemonServer(pkcRpcUrl, ipfsGatewayEndpoint, mergedPkcOptions, {
389
+ allowPrivateKeyExport: flags.allowPrivateKeyExport
390
+ });
382
391
  startedOwnRpc = true;
383
392
  console.log(`pkc rpc: listening on ${pkcRpcUrl} (local connections only)`);
384
393
  console.log(`pkc rpc: listening on ${pkcRpcUrl}${daemonServer.rpcAuthKey} (secret auth key for remote connections)`);
@@ -399,10 +408,6 @@ export default class Daemon extends Command {
399
408
  console.log(`WebUI (${webui.name}${desc}): http://${remoteIpAddress}:${rpcPort}${webui.endpointRemote}`);
400
409
  }
401
410
  };
402
- // RPC port was already verified free above (fail-fast); only the kuboRpcClientsOptions branch skips local kubo.
403
- if (!pkcOptionsFromFlag?.kuboRpcClientsOptions)
404
- await keepKuboUp();
405
- await createOrConnectRpc();
406
411
  let keepKuboUpInterval;
407
412
  const { asyncExitHook } = await import("exit-hook");
408
413
  const killKuboProcessGroup = (pid, signal) => {
@@ -494,6 +499,10 @@ export default class Daemon extends Command {
494
499
  killKuboProcessGroup(kuboProcess.pid, "SIGKILL");
495
500
  }
496
501
  });
502
+ // RPC port was already verified free above (fail-fast); only the kuboRpcClientsOptions branch skips local kubo.
503
+ if (!pkcOptionsFromFlag?.kuboRpcClientsOptions)
504
+ await keepKuboUp();
505
+ await createOrConnectRpc();
497
506
  keepKuboUpInterval = setInterval(async () => {
498
507
  if (mainProcessExited)
499
508
  return;
@@ -115,6 +115,7 @@ export default class Install extends Command {
115
115
  // Restart daemons with the new binary
116
116
  if (aliveDaemons.length > 0 && flags["restart-daemons"]) {
117
117
  await this._restartDaemons(aliveDaemons);
118
+ this.log("To see the daemon logs run `bitsocial logs --stdout`");
118
119
  }
119
120
  }
120
121
  async _restartDaemons(daemons) {
@@ -1,4 +1,6 @@
1
- export declare function startDaemonServer(rpcUrl: URL, ipfsGatewayUrl: URL, pkcOptions: any): Promise<{
1
+ export declare function startDaemonServer(rpcUrl: URL, ipfsGatewayUrl: URL, pkcOptions: any, rpcServerOptions?: {
2
+ allowPrivateKeyExport?: boolean;
3
+ }): Promise<{
2
4
  rpcAuthKey: string;
3
5
  listedSub: string[];
4
6
  webuis: {
@@ -7,10 +7,11 @@ import { PKCLogger } from "../util.js";
7
7
  import { randomBytes } from "crypto";
8
8
  import express from "express";
9
9
  import { loadChallengesIntoPKC } from "../challenge-packages/challenge-utils.js";
10
+ const rootHashRedirectScriptPattern = /<script\b[^>]*>(?:(?!<\/script>)[\s\S])*?window\.location\.replace\(["']\/#["']\s*\+\s*window\.location\.pathname\s*\+\s*window\.location\.search\);(?:(?!<\/script>)[\s\S])*?<\/script>/;
10
11
  async function _generateModifiedIndexHtmlWithRpcSettings(webuiPath, webuiName, ipfsGatewayPort) {
11
12
  const indexHtmlString = (await fs.readFile(path.join(webuiPath, "index_backup_no_rpc.html")))
12
13
  .toString()
13
- .replace(/<script>\s*\/\/\s*Redirect non-hash URLs[\s\S]*?<\/script>/, "");
14
+ .replace(rootHashRedirectScriptPattern, "");
14
15
  const defaultRpcOptionString = `[window.location.origin.replace("https://", "wss://").replace("http://", "ws://") + window.location.pathname.split('/' + '${webuiName}')[0]]`;
15
16
  // Ipfs media only locally because ipfs gateway doesn't allow remote connections
16
17
  const defaultIpfsMedia = `if (window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1" || window.location.hostname === "0.0.0.0")window.defaultMediaIpfsGatewayUrl = 'http://' + window.location.hostname + ':' + ${ipfsGatewayPort}`;
@@ -38,10 +39,23 @@ async function _generateRpcAuthKeyIfNotExisting(pkcDataPath) {
38
39
  return pkcRpcAuthKey;
39
40
  }
40
41
  // The daemon server will host both RPC and webui on the same port
41
- export async function startDaemonServer(rpcUrl, ipfsGatewayUrl, pkcOptions) {
42
+ export async function startDaemonServer(rpcUrl, ipfsGatewayUrl, pkcOptions, rpcServerOptions) {
42
43
  // Start pkc-js RPC
43
44
  const log = PKCLogger("bitsocial-cli:daemon:startDaemonServer");
44
45
  const webuiExpressApp = express();
46
+ // GET /exports/<exportId> is streamed by pkc-js's own request listener, attached to this same
47
+ // http.Server inside PKCWsServer. Express must stay silent for those paths — its catch-all 404
48
+ // races the async pkc-js handler and clobbers the download (the CLI's `community export` would
49
+ // see HTTP 404). Other /exports/ paths fall through to express's 404 because pkc-js ignores
50
+ // them on a caller-supplied server and the request would otherwise hang unanswered.
51
+ // NOT mounted at "/exports": a mounted middleware strips the mount prefix from the shared
52
+ // req.url while the request is held, so pkc-js's listener would no longer recognize it.
53
+ webuiExpressApp.use((req, res, next) => {
54
+ const isExportDownload = /^\/exports\/[0-9a-fA-F-]{36}$/.test(req.path);
55
+ if (!isExportDownload)
56
+ return next();
57
+ // intentionally neither responds nor calls next(): pkc-js's listener owns this request
58
+ });
45
59
  // Wait for bind to actually complete before returning. Calling express.listen() without
46
60
  // awaiting 'listening' lets startup proceed before the port is accepting connections,
47
61
  // and without an 'error' handler a bind failure becomes an uncaughtException that kills
@@ -67,7 +81,8 @@ export async function startDaemonServer(rpcUrl, ipfsGatewayUrl, pkcOptions) {
67
81
  const rpcServer = await PKCRpc.default.PKCWsServer({
68
82
  server: httpServer,
69
83
  pkcOptions: pkcOptions,
70
- authKey: rpcAuthKey
84
+ authKey: rpcAuthKey,
85
+ allowPrivateKeyExport: rpcServerOptions?.allowPrivateKeyExport
71
86
  });
72
87
  const webuisDir = path.join(__dirname, "..", "..", "dist", "webuis");
73
88
  const webUiNames = (await fs.readdir(webuisDir, { withFileTypes: true })).filter((file) => file.isDirectory()).map((file) => file.name);
@@ -9,7 +9,8 @@
9
9
  "bitsocial daemon --pkcRpcUrl ws://localhost:53812",
10
10
  "bitsocial daemon --pkcOptions.dataPath /tmp/bitsocial-datapath/",
11
11
  "bitsocial daemon --pkcOptions.kuboRpcClientsOptions[0] https://remoteipfsnode.com",
12
- "bitsocial daemon --chainProviderUrls https://mainnet.infura.io/v3/YOUR_KEY"
12
+ "bitsocial daemon --chainProviderUrls https://mainnet.infura.io/v3/YOUR_KEY",
13
+ "bitsocial daemon --no-allowPrivateKeyExport"
13
14
  ],
14
15
  "flags": {
15
16
  "pkcRpcUrl": {
@@ -44,6 +45,12 @@
44
45
  "hasDynamicHelp": false,
45
46
  "multiple": true,
46
47
  "type": "option"
48
+ },
49
+ "allowPrivateKeyExport": {
50
+ "description": "Allow RPC clients to request community exports that include the community signer's private key (`bitsocial community export --includePrivateKey`). Disable with --no-allowPrivateKeyExport when exposing the RPC to untrusted clients",
51
+ "name": "allowPrivateKeyExport",
52
+ "allowNo": true,
53
+ "type": "boolean"
47
54
  }
48
55
  },
49
56
  "hasDynamicHelp": false,
@@ -459,6 +466,91 @@
459
466
  "edit.js"
460
467
  ]
461
468
  },
469
+ "community:export": {
470
+ "aliases": [],
471
+ "args": {
472
+ "address": {
473
+ "description": "Address of the community to export",
474
+ "name": "address",
475
+ "required": false
476
+ }
477
+ },
478
+ "description": "Export a local community to a SQLite snapshot file. The export runs on the RPC server (daemon); once finished the snapshot is downloaded and its sha256 checksum is verified. Pass --includePrivateKey to produce a restorable backup that keeps the community's address.",
479
+ "examples": [
480
+ "bitsocial community export plebmusic.bso",
481
+ "bitsocial community export plebmusic.bso --includePrivateKey -o ./backups/plebmusic.sqlite",
482
+ "bitsocial community export --name my-community",
483
+ "bitsocial community export --publicKey 12D3KooWG3XbzoVyAE6Y9vHZKF64Yuuu4TjdgQKedk14iYmTEPWu"
484
+ ],
485
+ "flags": {
486
+ "pkcRpcUrl": {
487
+ "name": "pkcRpcUrl",
488
+ "required": true,
489
+ "summary": "URL to PKC RPC",
490
+ "default": "ws://localhost:9138/",
491
+ "hasDynamicHelp": false,
492
+ "multiple": false,
493
+ "type": "option"
494
+ },
495
+ "name": {
496
+ "description": "Name of the community to export",
497
+ "name": "name",
498
+ "hasDynamicHelp": false,
499
+ "multiple": false,
500
+ "type": "option"
501
+ },
502
+ "publicKey": {
503
+ "description": "Public key of the community to export",
504
+ "name": "publicKey",
505
+ "hasDynamicHelp": false,
506
+ "multiple": false,
507
+ "type": "option"
508
+ },
509
+ "path": {
510
+ "char": "o",
511
+ "description": "Destination file for the downloaded snapshot (default: <dataPath>/exports/<address>_<datetime>.sqlite)",
512
+ "name": "path",
513
+ "hasDynamicHelp": false,
514
+ "multiple": false,
515
+ "type": "option"
516
+ },
517
+ "includePrivateKey": {
518
+ "description": "Ask the RPC server to include the community signer's private key in the export. Required for a restorable backup that keeps the same community address. The daemon may refuse (see `bitsocial daemon --no-allowPrivateKeyExport`)",
519
+ "name": "includePrivateKey",
520
+ "allowNo": false,
521
+ "type": "boolean"
522
+ },
523
+ "force": {
524
+ "description": "Overwrite the destination file if it already exists",
525
+ "name": "force",
526
+ "allowNo": false,
527
+ "type": "boolean"
528
+ },
529
+ "quiet": {
530
+ "char": "q",
531
+ "description": "Suppress progress output; only print the path of the downloaded snapshot",
532
+ "name": "quiet",
533
+ "allowNo": false,
534
+ "type": "boolean"
535
+ }
536
+ },
537
+ "hasDynamicHelp": false,
538
+ "hiddenAliases": [],
539
+ "id": "community:export",
540
+ "pluginAlias": "@bitsocial/bitsocial-cli",
541
+ "pluginName": "@bitsocial/bitsocial-cli",
542
+ "pluginType": "core",
543
+ "strict": true,
544
+ "enableJsonFlag": false,
545
+ "isESM": true,
546
+ "relativePath": [
547
+ "dist",
548
+ "cli",
549
+ "commands",
550
+ "community",
551
+ "export.js"
552
+ ]
553
+ },
462
554
  "community:get": {
463
555
  "aliases": [],
464
556
  "args": {
@@ -770,5 +862,5 @@
770
862
  ]
771
863
  }
772
864
  },
773
- "version": "0.19.63"
865
+ "version": "0.19.65"
774
866
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitsocial/bitsocial-cli",
3
- "version": "0.19.63",
3
+ "version": "0.19.65",
4
4
  "description": "Command line interface to Bitsocial API",
5
5
  "types": "./dist/index.d.ts",
6
6
  "homepage": "https://github.com/bitsocialnet/bitsocial-cli",
@@ -119,7 +119,7 @@
119
119
  "@oclif/plugin-help": "6.2.36",
120
120
  "@oclif/plugin-not-found": "3.2.73",
121
121
  "@oclif/table": "0.5.1",
122
- "@pkcprotocol/pkc-js": "0.0.38",
122
+ "@pkcprotocol/pkc-js": "0.0.41",
123
123
  "dataobject-parser": "1.2.22",
124
124
  "decompress": "4.2.1",
125
125
  "env-paths": "2.2.1",