@bitsocial/bitsocial-cli 0.19.39
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/LICENSE +674 -0
- package/README.md +706 -0
- package/bin/dev +20 -0
- package/bin/dev.cmd +3 -0
- package/bin/postinstall.js +125 -0
- package/bin/run +13 -0
- package/bin/run.cmd +3 -0
- package/dist/challenge-packages/challenge-utils.d.ts +24 -0
- package/dist/challenge-packages/challenge-utils.js +304 -0
- package/dist/cli/base-command.d.ts +11 -0
- package/dist/cli/base-command.js +45 -0
- package/dist/cli/commands/challenge/install.d.ts +12 -0
- package/dist/cli/commands/challenge/install.js +131 -0
- package/dist/cli/commands/challenge/list.d.ts +10 -0
- package/dist/cli/commands/challenge/list.js +37 -0
- package/dist/cli/commands/challenge/remove.d.ts +12 -0
- package/dist/cli/commands/challenge/remove.js +60 -0
- package/dist/cli/commands/community/create.d.ts +12 -0
- package/dist/cli/commands/community/create.js +54 -0
- package/dist/cli/commands/community/delete.d.ts +10 -0
- package/dist/cli/commands/community/delete.js +44 -0
- package/dist/cli/commands/community/edit.d.ts +12 -0
- package/dist/cli/commands/community/edit.js +74 -0
- package/dist/cli/commands/community/get.d.ts +9 -0
- package/dist/cli/commands/community/get.js +32 -0
- package/dist/cli/commands/community/list.d.ts +9 -0
- package/dist/cli/commands/community/list.js +30 -0
- package/dist/cli/commands/community/start.d.ts +13 -0
- package/dist/cli/commands/community/start.js +46 -0
- package/dist/cli/commands/community/stop.d.ts +10 -0
- package/dist/cli/commands/community/stop.js +44 -0
- package/dist/cli/commands/daemon.d.ts +14 -0
- package/dist/cli/commands/daemon.js +484 -0
- package/dist/cli/commands/logs.d.ts +24 -0
- package/dist/cli/commands/logs.js +199 -0
- package/dist/cli/commands/subplebbit/create.d.ts +12 -0
- package/dist/cli/commands/subplebbit/create.js +54 -0
- package/dist/cli/commands/subplebbit/edit.d.ts +12 -0
- package/dist/cli/commands/subplebbit/edit.js +73 -0
- package/dist/cli/commands/subplebbit/get.d.ts +9 -0
- package/dist/cli/commands/subplebbit/get.js +32 -0
- package/dist/cli/commands/subplebbit/list.d.ts +9 -0
- package/dist/cli/commands/subplebbit/list.js +30 -0
- package/dist/cli/commands/subplebbit/start.d.ts +10 -0
- package/dist/cli/commands/subplebbit/start.js +41 -0
- package/dist/cli/commands/subplebbit/stop.d.ts +10 -0
- package/dist/cli/commands/subplebbit/stop.js +43 -0
- package/dist/cli/commands/update/check.d.ts +6 -0
- package/dist/cli/commands/update/check.js +28 -0
- package/dist/cli/commands/update/install.d.ts +12 -0
- package/dist/cli/commands/update/install.js +63 -0
- package/dist/cli/commands/update/versions.d.ts +9 -0
- package/dist/cli/commands/update/versions.js +29 -0
- package/dist/cli/hooks/init/version-hook.d.ts +3 -0
- package/dist/cli/hooks/init/version-hook.js +43 -0
- package/dist/cli/hooks/prerun/parse-dynamic-flags-hook.d.ts +3 -0
- package/dist/cli/hooks/prerun/parse-dynamic-flags-hook.js +94 -0
- package/dist/cli/types.d.ts +4 -0
- package/dist/cli/types.js +1 -0
- package/dist/common-utils/data-migration.d.ts +1 -0
- package/dist/common-utils/data-migration.js +27 -0
- package/dist/common-utils/defaults.d.ts +9 -0
- package/dist/common-utils/defaults.js +10 -0
- package/dist/common-utils/resolvers.d.ts +2 -0
- package/dist/common-utils/resolvers.js +6 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/ipfs/startIpfs.d.ts +3 -0
- package/dist/ipfs/startIpfs.js +304 -0
- package/dist/seeder.d.ts +1 -0
- package/dist/seeder.js +83 -0
- package/dist/update/npm-registry.d.ts +6 -0
- package/dist/update/npm-registry.js +66 -0
- package/dist/update/semver.d.ts +5 -0
- package/dist/update/semver.js +29 -0
- package/dist/util.d.ts +31 -0
- package/dist/util.js +157 -0
- package/dist/webui/daemon-server.d.ts +10 -0
- package/dist/webui/daemon-server.js +140 -0
- package/package.json +143 -0
package/dist/seeder.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// import lodash from "lodash";
|
|
2
|
+
// import Logger from "@plebbit/plebbit-logger";
|
|
3
|
+
// import { Plebbit } from "@plebbit/plebbit-js/dist/node/plebbit";
|
|
4
|
+
// import { BasePages } from "@plebbit/plebbit-js/dist/node/pages";
|
|
5
|
+
// import { Comment } from "@plebbit/plebbit-js/dist/node/comment";
|
|
6
|
+
// import assert from "assert";
|
|
7
|
+
// //@ts-expect-error
|
|
8
|
+
// import { CID } from "ipfs-http-client";
|
|
9
|
+
// import { Subplebbit } from "@plebbit/plebbit-js/dist/node/subplebbit/subplebbit";
|
|
10
|
+
export {};
|
|
11
|
+
// async function _loadAllPages(pageCid: string, pagesInstance: BasePages): Promise<Comment[]> {
|
|
12
|
+
// const log = Logger("plebbit-cli:server:seed:_loadAllPages");
|
|
13
|
+
// try {
|
|
14
|
+
// let sortedCommentsPage = await pagesInstance.getPage(pageCid);
|
|
15
|
+
// let sortedComments: Comment[] = sortedCommentsPage.comments;
|
|
16
|
+
// while (sortedCommentsPage.nextCid) {
|
|
17
|
+
// sortedCommentsPage = await pagesInstance.getPage(sortedCommentsPage.nextCid);
|
|
18
|
+
// sortedComments = sortedComments.concat(sortedCommentsPage.comments);
|
|
19
|
+
// }
|
|
20
|
+
// return sortedComments;
|
|
21
|
+
// } catch (e) {
|
|
22
|
+
// log.error(`Failed to load page (${pageCid}) of sub (${pagesInstance._subplebbitAddress}) due to error:`, e);
|
|
23
|
+
// return [];
|
|
24
|
+
// }
|
|
25
|
+
// }
|
|
26
|
+
// const seededIpns: Record<string, { lastSeededAt?: number }> = {};
|
|
27
|
+
// async function _seedSub(sub: Subplebbit, pinnedCids: string[]) {
|
|
28
|
+
// const log = Logger("plebbit-cli:server:seed");
|
|
29
|
+
// if (sub.statsCid) await sub.plebbit.fetchCid(sub.statsCid); // Seed stats
|
|
30
|
+
// await sub.plebbit.pubsubSubscribe(sub.pubsubTopic || sub.address);
|
|
31
|
+
// // Load all pages
|
|
32
|
+
// if (sub.posts.pageCids) {
|
|
33
|
+
// const pagesLoaded = await Promise.all(Object.values(sub.posts.pageCids).map((pageCid) => _loadAllPages(pageCid, sub.posts)));
|
|
34
|
+
// // What if one of pages fail to load
|
|
35
|
+
// log.trace(`Loaded the newest pages of sub (${sub.address}) to seed`);
|
|
36
|
+
// const pageNames = Object.keys(sub.posts.pageCids);
|
|
37
|
+
// const loadedPagesWithNames = lodash.zipObject(pageNames, pagesLoaded);
|
|
38
|
+
// const allCidsToPin: string[] = [];
|
|
39
|
+
// if (loadedPagesWithNames["new"]) {
|
|
40
|
+
// // Fetch all comments CID
|
|
41
|
+
// allCidsToPin.push(...loadedPagesWithNames["new"].map((comment) => <string>comment.cid));
|
|
42
|
+
// // Seed IPNS
|
|
43
|
+
// for (const comment of loadedPagesWithNames["new"]) {
|
|
44
|
+
// assert(comment.ipnsName);
|
|
45
|
+
// if (seededIpns[comment.ipnsName]?.lastSeededAt !== comment.updatedAt) {
|
|
46
|
+
// try {
|
|
47
|
+
// await comment._clientsManager.fetchCommentUpdate(comment.ipnsName);
|
|
48
|
+
// seededIpns[comment.ipnsName] = { lastSeededAt: comment.updatedAt };
|
|
49
|
+
// log.trace(`Seeded comment (${comment.cid}) IPNS (${comment.ipnsName})`);
|
|
50
|
+
// } catch (e) {
|
|
51
|
+
// log.error(`Failed to seed comment (${comment.cid}) IPNS (${comment.ipnsName}) due to error`, e);
|
|
52
|
+
// }
|
|
53
|
+
// }
|
|
54
|
+
// }
|
|
55
|
+
// }
|
|
56
|
+
// // Pin cids that are not already pinned
|
|
57
|
+
// const newCidsToPin = lodash.difference(lodash.uniq(allCidsToPin), pinnedCids).map((cidString) => CID.parse(cidString));
|
|
58
|
+
// if (newCidsToPin.length > 0) {
|
|
59
|
+
// log.trace(`Attempting to pin ${newCidsToPin.length} comments' cids from sub (${sub.address}): `, newCidsToPin);
|
|
60
|
+
// const defaultIpfsClient = Object.values(sub.plebbit.clients.ipfsClients)[0];
|
|
61
|
+
// assert(defaultIpfsClient);
|
|
62
|
+
// await defaultIpfsClient._client.pin.addAll(newCidsToPin);
|
|
63
|
+
// log.trace(`Pinned ${newCidsToPin.length} cids from sub (${sub.address})`);
|
|
64
|
+
// } else log.trace(`All ${lodash.uniq(allCidsToPin).length} cids from sub (${sub.address}) are pinned`);
|
|
65
|
+
// }
|
|
66
|
+
// }
|
|
67
|
+
// export async function seedSubplebbits(subAddresses: string[], plebbit: Plebbit) {
|
|
68
|
+
// const log = Logger("plebbit-cli:server:seed");
|
|
69
|
+
// log.trace("test"); // remove this line later
|
|
70
|
+
// for (const subAddress of subAddresses) {
|
|
71
|
+
// try {
|
|
72
|
+
// const sub = await plebbit.getSubplebbit(subAddress);
|
|
73
|
+
// log.trace(`Loaded the newest record of sub (${subAddress}) for seeding`);
|
|
74
|
+
// const defaultIpfsClient = Object.values(sub.plebbit.clients.ipfsClients)[0];
|
|
75
|
+
// assert(defaultIpfsClient);
|
|
76
|
+
// const pinnedCids: string[] = (await defaultIpfsClient._client.pin.ls()).map((pin) => pin.cid.toString());
|
|
77
|
+
// await _seedSub(sub, pinnedCids);
|
|
78
|
+
// } catch (e) {
|
|
79
|
+
// log.error(`Failed to load and seed sub (${subAddress}):`, String(e));
|
|
80
|
+
// }
|
|
81
|
+
// }
|
|
82
|
+
// log(`Finished this round of seeding. Will seed again later`);
|
|
83
|
+
// }
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Query npm registry for the latest published version. */
|
|
2
|
+
export declare function fetchLatestVersion(): Promise<string>;
|
|
3
|
+
/** Query npm registry for all published versions (oldest-first). */
|
|
4
|
+
export declare function fetchAllVersions(): Promise<string[]>;
|
|
5
|
+
/** Install a specific version globally via npm install -g. Streams output to terminal. */
|
|
6
|
+
export declare function installGlobal(version: string): Promise<void>;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import { getNpmCliPath, getNpmEnv, ensureNpmAvailable } from "../challenge-packages/challenge-utils.js";
|
|
3
|
+
const PACKAGE_NAME = "@bitsocial/bitsocial-cli";
|
|
4
|
+
function runNpmView(args) {
|
|
5
|
+
return new Promise(async (resolve, reject) => {
|
|
6
|
+
const npmCliPath = await getNpmCliPath();
|
|
7
|
+
const proc = spawn(process.execPath, [npmCliPath, "view", PACKAGE_NAME, ...args, "--json"], {
|
|
8
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
9
|
+
env: getNpmEnv()
|
|
10
|
+
});
|
|
11
|
+
let stdout = "";
|
|
12
|
+
let stderr = "";
|
|
13
|
+
proc.stdout.on("data", (data) => {
|
|
14
|
+
stdout += data.toString();
|
|
15
|
+
});
|
|
16
|
+
proc.stderr.on("data", (data) => {
|
|
17
|
+
stderr += data.toString();
|
|
18
|
+
});
|
|
19
|
+
proc.on("error", (err) => {
|
|
20
|
+
reject(new Error(`Failed to run npm view: ${err.message}`));
|
|
21
|
+
});
|
|
22
|
+
proc.on("close", (code) => {
|
|
23
|
+
if (code === 0) {
|
|
24
|
+
resolve(stdout.trim());
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
reject(new Error(`npm view exited with code ${code}: ${stderr.trim()}`));
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
/** Query npm registry for the latest published version. */
|
|
33
|
+
export async function fetchLatestVersion() {
|
|
34
|
+
await ensureNpmAvailable();
|
|
35
|
+
const raw = await runNpmView(["version"]);
|
|
36
|
+
// npm view <pkg> version --json returns a quoted string like "0.19.40"
|
|
37
|
+
return JSON.parse(raw);
|
|
38
|
+
}
|
|
39
|
+
/** Query npm registry for all published versions (oldest-first). */
|
|
40
|
+
export async function fetchAllVersions() {
|
|
41
|
+
await ensureNpmAvailable();
|
|
42
|
+
const raw = await runNpmView(["versions"]);
|
|
43
|
+
return JSON.parse(raw);
|
|
44
|
+
}
|
|
45
|
+
/** Install a specific version globally via npm install -g. Streams output to terminal. */
|
|
46
|
+
export async function installGlobal(version) {
|
|
47
|
+
await ensureNpmAvailable();
|
|
48
|
+
const npmCliPath = await getNpmCliPath();
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
const proc = spawn(process.execPath, [npmCliPath, "install", "-g", `${PACKAGE_NAME}@${version}`], {
|
|
51
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
52
|
+
env: getNpmEnv()
|
|
53
|
+
});
|
|
54
|
+
proc.stdout?.pipe(process.stdout);
|
|
55
|
+
proc.stderr?.pipe(process.stderr);
|
|
56
|
+
proc.on("error", (err) => {
|
|
57
|
+
reject(new Error(`Failed to run npm install: ${err.message}`));
|
|
58
|
+
});
|
|
59
|
+
proc.on("close", (code) => {
|
|
60
|
+
if (code === 0)
|
|
61
|
+
resolve();
|
|
62
|
+
else
|
|
63
|
+
reject(new Error(`npm install -g exited with code ${code}`));
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal semver comparison. Strips leading "v", splits on ".",
|
|
3
|
+
* compares each segment numerically left-to-right.
|
|
4
|
+
*/
|
|
5
|
+
export function compareVersions(a, b) {
|
|
6
|
+
const normalize = (v) => v.replace(/^v/i, "");
|
|
7
|
+
const partsA = normalize(a).split(".").map(Number);
|
|
8
|
+
const partsB = normalize(b).split(".").map(Number);
|
|
9
|
+
const len = Math.max(partsA.length, partsB.length);
|
|
10
|
+
for (let i = 0; i < len; i++) {
|
|
11
|
+
const na = partsA[i] ?? 0;
|
|
12
|
+
const nb = partsB[i] ?? 0;
|
|
13
|
+
if (Number.isNaN(na) || Number.isNaN(nb)) {
|
|
14
|
+
// Fall back to string compare for non-numeric segments
|
|
15
|
+
const sa = String(partsA[i] ?? "");
|
|
16
|
+
const sb = String(partsB[i] ?? "");
|
|
17
|
+
if (sa < sb)
|
|
18
|
+
return -1;
|
|
19
|
+
if (sa > sb)
|
|
20
|
+
return 1;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (na < nb)
|
|
24
|
+
return -1;
|
|
25
|
+
if (na > nb)
|
|
26
|
+
return 1;
|
|
27
|
+
}
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
package/dist/util.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type PKCLogger = Awaited<ReturnType<typeof getPKCLogger>> & {
|
|
2
|
+
inspectOpts?: {
|
|
3
|
+
depth?: number;
|
|
4
|
+
colors?: boolean;
|
|
5
|
+
[key: string]: any;
|
|
6
|
+
};
|
|
7
|
+
};
|
|
8
|
+
export declare function getPKCLogger(): Promise<typeof import("@pkcprotocol/pkc-logger").default>;
|
|
9
|
+
/**
|
|
10
|
+
* Read _PKC_DEBUG / DEBUG env vars and configure the Logger instance.
|
|
11
|
+
* Does NOT redirect output — debug logs go to stderr (the default for the debug module).
|
|
12
|
+
*
|
|
13
|
+
* @param options.enableDefaultNamespace - If true, enable "bitsocial*,pkc*,-pkc*trace"
|
|
14
|
+
* when no DEBUG env is set (used by daemon). If false, only enable if user
|
|
15
|
+
* explicitly set DEBUG or _PKC_DEBUG (used by non-daemon commands).
|
|
16
|
+
*/
|
|
17
|
+
export declare function setupDebugLogger(Logger: PKCLogger, options?: {
|
|
18
|
+
enableDefaultNamespace?: boolean;
|
|
19
|
+
}): {
|
|
20
|
+
debugNamespace: string | undefined;
|
|
21
|
+
debugDepth: number;
|
|
22
|
+
};
|
|
23
|
+
export declare function getLanIpV4Address(): string | undefined;
|
|
24
|
+
export declare function loadKuboConfigFile(pkcDataPath: string): Promise<any | undefined>;
|
|
25
|
+
export declare function parseMultiAddrKuboRpcToUrl(kuboMultiAddrString: string): Promise<import("url").URL>;
|
|
26
|
+
export declare function parseMultiAddrIpfsGatewayToUrl(ipfsGatewaymultiAddrString: string): Promise<import("url").URL>;
|
|
27
|
+
/**
|
|
28
|
+
* Custom merge function that implements CLI-specific merge behavior.
|
|
29
|
+
* This matches the expected behavior from the test suite.
|
|
30
|
+
*/
|
|
31
|
+
export declare function mergeDeep(target: any, source: any): any;
|
package/dist/util.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import os from "os";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import * as fsPromises from "fs/promises";
|
|
5
|
+
export async function getPKCLogger() {
|
|
6
|
+
const Logger = await import("@pkcprotocol/pkc-logger");
|
|
7
|
+
return Logger.default;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Read _PKC_DEBUG / DEBUG env vars and configure the Logger instance.
|
|
11
|
+
* Does NOT redirect output — debug logs go to stderr (the default for the debug module).
|
|
12
|
+
*
|
|
13
|
+
* @param options.enableDefaultNamespace - If true, enable "bitsocial*,pkc*,-pkc*trace"
|
|
14
|
+
* when no DEBUG env is set (used by daemon). If false, only enable if user
|
|
15
|
+
* explicitly set DEBUG or _PKC_DEBUG (used by non-daemon commands).
|
|
16
|
+
*/
|
|
17
|
+
export function setupDebugLogger(Logger, options = {}) {
|
|
18
|
+
const envDebug = process.env["_PKC_DEBUG"] || process.env["DEBUG"];
|
|
19
|
+
const debugNamespace = envDebug === "0" || envDebug === "" ? undefined : envDebug;
|
|
20
|
+
const debugDepth = process.env["DEBUG_DEPTH"] ? parseInt(process.env["DEBUG_DEPTH"]) : 10;
|
|
21
|
+
Logger.inspectOpts = Logger.inspectOpts || {};
|
|
22
|
+
Logger.inspectOpts.depth = debugDepth;
|
|
23
|
+
const defaultNamespace = "bitsocial*,pkc*,-pkc*trace";
|
|
24
|
+
if (debugNamespace) {
|
|
25
|
+
Logger.enable(debugNamespace);
|
|
26
|
+
}
|
|
27
|
+
else if (options.enableDefaultNamespace) {
|
|
28
|
+
Logger.enable(defaultNamespace);
|
|
29
|
+
}
|
|
30
|
+
return { debugNamespace, debugDepth };
|
|
31
|
+
}
|
|
32
|
+
export function getLanIpV4Address() {
|
|
33
|
+
const allInterfaces = os.networkInterfaces();
|
|
34
|
+
for (const k in allInterfaces) {
|
|
35
|
+
const specificInterfaceInfos = allInterfaces[k];
|
|
36
|
+
if (!specificInterfaceInfos)
|
|
37
|
+
continue;
|
|
38
|
+
const lanAddress = specificInterfaceInfos.filter((info) => info.family === "IPv4" && !info.internal)[0]
|
|
39
|
+
?.address;
|
|
40
|
+
if (lanAddress)
|
|
41
|
+
return lanAddress;
|
|
42
|
+
}
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
export async function loadKuboConfigFile(pkcDataPath) {
|
|
46
|
+
const kuboConfigPath = path.join(pkcDataPath, ".ipfs-bitsocial-cli", "config");
|
|
47
|
+
if (!fs.existsSync(kuboConfigPath))
|
|
48
|
+
return undefined;
|
|
49
|
+
const kuboConfig = JSON.parse((await fsPromises.readFile(kuboConfigPath)).toString());
|
|
50
|
+
return kuboConfig;
|
|
51
|
+
}
|
|
52
|
+
async function parseMultiAddr(multiAddrString) {
|
|
53
|
+
const module = await import("@multiformats/multiaddr");
|
|
54
|
+
return module.multiaddr(multiAddrString);
|
|
55
|
+
}
|
|
56
|
+
function multiAddrToHostPort(multiAddrObj) {
|
|
57
|
+
const components = multiAddrObj.getComponents();
|
|
58
|
+
const hostComponent = components.find((component) => ["ip4", "ip6", "dns", "dns4", "dns6", "dnsaddr"].includes(component.name));
|
|
59
|
+
const tcpComponent = components.find((component) => component.name === "tcp");
|
|
60
|
+
const host = hostComponent?.value;
|
|
61
|
+
const port = tcpComponent?.value ? Number(tcpComponent.value) : undefined;
|
|
62
|
+
if (!host || !port || !Number.isFinite(port) || port <= 0)
|
|
63
|
+
return undefined;
|
|
64
|
+
return { host, port };
|
|
65
|
+
}
|
|
66
|
+
export async function parseMultiAddrKuboRpcToUrl(kuboMultiAddrString) {
|
|
67
|
+
const multiAddrObj = await parseMultiAddr(kuboMultiAddrString);
|
|
68
|
+
const parsed = multiAddrToHostPort(multiAddrObj);
|
|
69
|
+
if (!parsed)
|
|
70
|
+
throw new Error(`Unable to parse kubo RPC multiaddr: ${kuboMultiAddrString}`);
|
|
71
|
+
return new URL(`http://${parsed.host}:${parsed.port}/api/v0`);
|
|
72
|
+
}
|
|
73
|
+
export async function parseMultiAddrIpfsGatewayToUrl(ipfsGatewaymultiAddrString) {
|
|
74
|
+
const multiAddrObj = await parseMultiAddr(ipfsGatewaymultiAddrString);
|
|
75
|
+
const parsed = multiAddrToHostPort(multiAddrObj);
|
|
76
|
+
if (!parsed)
|
|
77
|
+
throw new Error(`Unable to parse IPFS gateway multiaddr: ${ipfsGatewaymultiAddrString}`);
|
|
78
|
+
return new URL(`http://${parsed.host}:${parsed.port}`);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Custom merge function that implements CLI-specific merge behavior.
|
|
82
|
+
* This matches the expected behavior from the test suite.
|
|
83
|
+
*/
|
|
84
|
+
export function mergeDeep(target, source) {
|
|
85
|
+
function isObject(item) {
|
|
86
|
+
return item && typeof item === "object" && !Array.isArray(item);
|
|
87
|
+
}
|
|
88
|
+
function isPlainObject(item) {
|
|
89
|
+
return isObject(item) && item.constructor === Object;
|
|
90
|
+
}
|
|
91
|
+
// Handle arrays with CLI-specific behavior
|
|
92
|
+
if (Array.isArray(target) && Array.isArray(source)) {
|
|
93
|
+
// Check if source is sparse (has holes/empty items) - indicates indexed assignment like --rules[2]
|
|
94
|
+
const sourceHasHoles = source.length !== Object.keys(source).length;
|
|
95
|
+
if (sourceHasHoles) {
|
|
96
|
+
// Sparse array: merge by index, extending to accommodate both arrays
|
|
97
|
+
const maxLength = Math.max(target.length, source.length);
|
|
98
|
+
const result = new Array(maxLength);
|
|
99
|
+
for (let i = 0; i < maxLength; i++) {
|
|
100
|
+
if (i in source) {
|
|
101
|
+
if (i in target && isPlainObject(target[i]) && isPlainObject(source[i])) {
|
|
102
|
+
result[i] = mergeDeep(target[i], source[i]);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
result[i] = source[i];
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
else if (i in target) {
|
|
109
|
+
result[i] = target[i];
|
|
110
|
+
}
|
|
111
|
+
// If neither has this index, it remains undefined
|
|
112
|
+
}
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
// Dense array: CLI behavior is to extend the array to include both original and new values
|
|
117
|
+
// This creates: [source[0], source[1], target[2], target[3], ...]
|
|
118
|
+
const maxLength = target.length + source.length;
|
|
119
|
+
const result = new Array(maxLength);
|
|
120
|
+
// First, place source values at the beginning
|
|
121
|
+
for (let i = 0; i < source.length; i++) {
|
|
122
|
+
result[i] = source[i];
|
|
123
|
+
}
|
|
124
|
+
// Then, place target values at their original indices (beyond source length)
|
|
125
|
+
for (let i = source.length; i < maxLength; i++) {
|
|
126
|
+
const targetIndex = i; // Use the same index, not shifted
|
|
127
|
+
if (targetIndex < target.length) {
|
|
128
|
+
result[i] = target[targetIndex];
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
result[i] = undefined;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// Handle plain objects
|
|
138
|
+
if (isPlainObject(target) && isPlainObject(source)) {
|
|
139
|
+
const result = { ...target };
|
|
140
|
+
for (const key in source) {
|
|
141
|
+
if (source.hasOwnProperty(key)) {
|
|
142
|
+
if (Array.isArray(target[key]) && Array.isArray(source[key])) {
|
|
143
|
+
result[key] = mergeDeep(target[key], source[key]);
|
|
144
|
+
}
|
|
145
|
+
else if (isPlainObject(target[key]) && isPlainObject(source[key])) {
|
|
146
|
+
result[key] = mergeDeep(target[key], source[key]);
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
result[key] = source[key];
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
// If not both objects/arrays, source takes precedence
|
|
156
|
+
return source;
|
|
157
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare function startDaemonServer(rpcUrl: URL, ipfsGatewayUrl: URL, pkcOptions: any): Promise<{
|
|
2
|
+
rpcAuthKey: string;
|
|
3
|
+
listedSub: string[];
|
|
4
|
+
webuis: {
|
|
5
|
+
name: string;
|
|
6
|
+
endpointLocal: string;
|
|
7
|
+
endpointRemote: string;
|
|
8
|
+
}[];
|
|
9
|
+
destroy: () => Promise<void>;
|
|
10
|
+
}>;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { fileURLToPath } from "url";
|
|
3
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
4
|
+
const __dirname = path.dirname(__filename);
|
|
5
|
+
import fs from "fs/promises";
|
|
6
|
+
import { getPKCLogger } from "../util.js";
|
|
7
|
+
import { randomBytes } from "crypto";
|
|
8
|
+
import express from "express";
|
|
9
|
+
import { loadChallengesIntoPKC } from "../challenge-packages/challenge-utils.js";
|
|
10
|
+
async function _generateModifiedIndexHtmlWithRpcSettings(webuiPath, webuiName, ipfsGatewayPort) {
|
|
11
|
+
const indexHtmlString = (await fs.readFile(path.join(webuiPath, "index_backup_no_rpc.html")))
|
|
12
|
+
.toString()
|
|
13
|
+
.replace(/<script>\s*\/\/\s*Redirect non-hash URLs[\s\S]*?<\/script>/, "");
|
|
14
|
+
const defaultRpcOptionString = `[window.location.origin.replace("https://", "wss://").replace("http://", "ws://") + window.location.pathname.split('/' + '${webuiName}')[0]]`;
|
|
15
|
+
// Ipfs media only locally because ipfs gateway doesn't allow remote connections
|
|
16
|
+
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}`;
|
|
17
|
+
const defaultOptionsString = `<script>window.defaultPkcOptions = {pkcRpcClientsOptions: ${defaultRpcOptionString}};${defaultIpfsMedia};console.log(window.defaultPkcOptions, window.defaultMediaIpfsGatewayUrl)</script>`;
|
|
18
|
+
const modifiedIndexHtmlContent = "<!DOCTYPE html>" + defaultOptionsString + indexHtmlString.replace("<!DOCTYPE html>", "");
|
|
19
|
+
return modifiedIndexHtmlContent;
|
|
20
|
+
}
|
|
21
|
+
async function _generateRpcAuthKeyIfNotExisting(pkcDataPath) {
|
|
22
|
+
const pkcRpcAuthKeyPath = path.join(pkcDataPath, "auth-key");
|
|
23
|
+
const envAuthKey = process.env["PKC_RPC_AUTH_KEY"];
|
|
24
|
+
let pkcRpcAuthKey;
|
|
25
|
+
if (envAuthKey) {
|
|
26
|
+
pkcRpcAuthKey = envAuthKey;
|
|
27
|
+
await fs.writeFile(pkcRpcAuthKeyPath, pkcRpcAuthKey);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
try {
|
|
31
|
+
pkcRpcAuthKey = await fs.readFile(pkcRpcAuthKeyPath, "utf-8");
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
pkcRpcAuthKey = randomBytes(32).toString("base64").replace(/[/+=]/g, "").substring(0, 40);
|
|
35
|
+
await fs.writeFile(pkcRpcAuthKeyPath, pkcRpcAuthKey, { flag: "wx" });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return pkcRpcAuthKey;
|
|
39
|
+
}
|
|
40
|
+
// The daemon server will host both RPC and webui on the same port
|
|
41
|
+
export async function startDaemonServer(rpcUrl, ipfsGatewayUrl, pkcOptions) {
|
|
42
|
+
// Start pkc-js RPC
|
|
43
|
+
const log = (await getPKCLogger())("bitsocial-cli:daemon:startDaemonServer");
|
|
44
|
+
const webuiExpressApp = express();
|
|
45
|
+
const httpServer = webuiExpressApp.listen(Number(rpcUrl.port));
|
|
46
|
+
log("HTTP server is running on", "0.0.0.0" + ":" + rpcUrl.port);
|
|
47
|
+
const rpcAuthKey = await _generateRpcAuthKeyIfNotExisting(pkcOptions.dataPath);
|
|
48
|
+
const PKCRpc = await import("@pkcprotocol/pkc-js/rpc");
|
|
49
|
+
// Will add ability to edit later, but it's hard coded for now
|
|
50
|
+
log("Will be passing pkc options to RPC server", pkcOptions);
|
|
51
|
+
const rpcServer = await PKCRpc.default.PKCWsServer({
|
|
52
|
+
server: httpServer,
|
|
53
|
+
pkcOptions: pkcOptions,
|
|
54
|
+
authKey: rpcAuthKey
|
|
55
|
+
});
|
|
56
|
+
const webuisDir = path.join(__dirname, "..", "..", "dist", "webuis");
|
|
57
|
+
const webUiNames = (await fs.readdir(webuisDir, { withFileTypes: true })).filter((file) => file.isDirectory()).map((file) => file.name);
|
|
58
|
+
const webuis = [];
|
|
59
|
+
log("Discovered webuis", webUiNames);
|
|
60
|
+
for (const webuiNameWithVersion of webUiNames) {
|
|
61
|
+
const webuiDirPath = path.join(webuisDir, webuiNameWithVersion);
|
|
62
|
+
const webuiName = webuiNameWithVersion.split("-")[0]; // should be "seedit", "plebones"
|
|
63
|
+
const modifiedIndexHtmlString = await _generateModifiedIndexHtmlWithRpcSettings(webuiDirPath, webuiName, Number(ipfsGatewayUrl.port));
|
|
64
|
+
const endpointLocal = `/${webuiName}`;
|
|
65
|
+
webuiExpressApp.use(endpointLocal, express.static(webuiDirPath, { index: false }));
|
|
66
|
+
webuiExpressApp.get(endpointLocal, (req, res, next) => {
|
|
67
|
+
const isLocal = req.socket.localAddress && req.socket.localAddress === req.socket.remoteAddress;
|
|
68
|
+
log("Received local connection request for webui", endpointLocal, "with socket.localAddress", req.socket.localAddress, "and socket.remoteAddress", req.socket.remoteAddress);
|
|
69
|
+
if (!isLocal)
|
|
70
|
+
res.status(403).send("This endpoint does not exist for remote connections");
|
|
71
|
+
else {
|
|
72
|
+
res.set("Cache-Control", "no-store, no-cache, must-revalidate, private");
|
|
73
|
+
res.set("Expires", "-1");
|
|
74
|
+
res.set("Pragma", "no-cache");
|
|
75
|
+
res.type("html").send(modifiedIndexHtmlString);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
const endpointRemote = `/${rpcAuthKey}/${webuiName}`;
|
|
79
|
+
webuiExpressApp.use(endpointRemote, express.static(webuiDirPath, { index: false }));
|
|
80
|
+
webuiExpressApp.get(endpointRemote, (req, res, next) => {
|
|
81
|
+
const isLocal = req.socket.localAddress && req.socket.localAddress === req.socket.remoteAddress;
|
|
82
|
+
log("Received remote connection request for webui", endpointLocal, "with socket.localAddress", req.socket.localAddress, "and socket.remoteAddress", req.socket.remoteAddress, "with req.url", req.url);
|
|
83
|
+
if (isLocal) {
|
|
84
|
+
res.redirect(`http://localhost:${rpcUrl.port}/${webuiName}`);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
res.set("Cache-Control", "public, max-age=600"); // 600 seconds = 10 minutes
|
|
88
|
+
res.type("html").send(modifiedIndexHtmlString);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
webuis.push({ name: webuiName, endpointLocal, endpointRemote });
|
|
92
|
+
}
|
|
93
|
+
// Challenge reload endpoints
|
|
94
|
+
const handleChallengeReload = async (_req, res) => {
|
|
95
|
+
try {
|
|
96
|
+
const loadedNames = await loadChallengesIntoPKC(pkcOptions.dataPath);
|
|
97
|
+
// Notify all connected RPC clients about the updated challenges
|
|
98
|
+
const onSettingsChange = rpcServer._onSettingsChange;
|
|
99
|
+
if (onSettingsChange) {
|
|
100
|
+
for (const connectionId of Object.keys(onSettingsChange)) {
|
|
101
|
+
const handlers = onSettingsChange[connectionId];
|
|
102
|
+
if (!handlers)
|
|
103
|
+
continue;
|
|
104
|
+
for (const subscriptionId of Object.keys(handlers)) {
|
|
105
|
+
const handler = handlers[subscriptionId];
|
|
106
|
+
if (handler)
|
|
107
|
+
await handler({ newPKC: rpcServer.pkc });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
res.json({ ok: true, challenges: loadedNames });
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
log.error("Failed to reload challenges", err);
|
|
115
|
+
res.status(500).json({ ok: false, error: String(err) });
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
// Local-only endpoint (same isLocal check as webui routes)
|
|
119
|
+
webuiExpressApp.post("/api/challenges/reload", (req, res) => {
|
|
120
|
+
const isLocal = req.socket.localAddress && req.socket.localAddress === req.socket.remoteAddress;
|
|
121
|
+
if (!isLocal) {
|
|
122
|
+
res.status(403).send("This endpoint does not exist for remote connections");
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
handleChallengeReload(req, res);
|
|
126
|
+
});
|
|
127
|
+
// Remote endpoint with auth key
|
|
128
|
+
webuiExpressApp.post(`/${rpcAuthKey}/api/challenges/reload`, (req, res) => {
|
|
129
|
+
handleChallengeReload(req, res);
|
|
130
|
+
});
|
|
131
|
+
let daemonServerDestroyed = false;
|
|
132
|
+
const cleanupDaemonServer = async () => {
|
|
133
|
+
if (daemonServerDestroyed)
|
|
134
|
+
return;
|
|
135
|
+
await rpcServer.destroy();
|
|
136
|
+
httpServer.close();
|
|
137
|
+
daemonServerDestroyed = true;
|
|
138
|
+
};
|
|
139
|
+
return { rpcAuthKey, listedSub: rpcServer.pkc.communities, webuis, destroy: cleanupDaemonServer };
|
|
140
|
+
}
|