import { Er as __toESM, Jn as require_commonjs, L as hideClassPrivateProps, ir as PKCError, or as logger_default, ot as replaceXWithY, yt as findStartedCommunity } from "../../chunks/schema-B7HIUI8w.js";
import { _ as parseRpcUnsubscribeParam, c as parseRpcCancelExportParam, d as parseRpcCommunityIdentifierParam, f as parseRpcCommunityPageParam, g as parseRpcPublishChallengeAnswersParam, h as parseRpcExportCommunityParam, l as parseRpcCidParam, m as parseRpcExportCommunityModLogsParam, p as parseRpcEditCommunityParam, s as parseRpcAuthorNameParam, t as PKC, u as parseRpcCommentRepliesPageParam } from "../../chunks/node-edb9-nES.js";
import { Cn as parseVoteChallengeRequestToEncryptSchemaWithPKCErrorIfItFails, G as buildPageRuntimeFields, Jt as parseCommentChallengeRequestToEncryptSchemaWithPKCErrorIfItFails, K as buildPagesRuntimeFields, Qt as parseCommentModerationChallengeRequestToEncryptSchemaWithPKCErrorIfItFails, Sn as parseSetNewSettingsPKCWsServerSchemaWithPKCErrorIfItFails, Un as stringify, Yt as parseCommentEditChallengeRequestToEncryptSchemaWithPKCErrorIfItFails, dn as parseCreatePKCWsServerOptionsSchemaWithPKCErrorIfItFails, hn as parseDecryptedChallengeAnswerWithPKCErrorIfItFails, nn as parseCommunityEditChallengeRequestToEncryptSchemaWithPKCErrorIfItFails, rn as parseCommunityEditOptionsSchemaWithPKCErrorIfItFails, rt as require_lib, tt as pLimit, un as parseCreateNewLocalCommunityUserOptionsSchemaWithPKCErrorIfItFails } from "../../chunks/rpc-local-community-CO__pwvD.js";
import { t as LocalCommunity } from "../../chunks/local-community-CXbUZ0um.js";
import { fileURLToPath } from "node:url";
import path from "path";
import assert from "assert";
import { createReadStream, existsSync, mkdirSync, promises, statSync } from "fs";
import http from "http";
import { randomInt } from "crypto";
import { toString } from "uint8arrays/to-string";
import Database from "better-sqlite3";
import { Server } from "rpc-websockets";
//#region dist/node/rpc/src/lib/pkc-js/index.js
const log$1 = logger_default("pkc-react-hooks:pkc-js");
const PKCJs = { PKC };
/**
* replace PKCJs with a different implementation, for
* example to mock it during unit tests, to add mock content
* for developing the front-end or to add a PKCJs with
* desktop privileges in the Electron build.
*/
function setPKCJs(_PKC) {
	assert(typeof _PKC === "function", `setPKCJs invalid PKC argument '${_PKC}' not a function`);
	if (_PKC.challenges === void 0) _PKC.challenges = PKC.challenges;
	PKCJs.PKC = _PKC;
	log$1("setPKCJs", _PKC?.constructor?.name);
}
//#endregion
//#region dist/node/rpc/src/utils.js
const maxRandomInt = 0xffffffffffff;
const generateSubscriptionId = () => randomInt(1, maxRandomInt);
function _encodeChallengeRequestId(id) {
	return toString(id, "base58btc");
}
function _encodeEncrypted(encrypted) {
	return {
		tag: toString(encrypted.tag, "base64"),
		iv: toString(encrypted.iv, "base64"),
		ciphertext: toString(encrypted.ciphertext, "base64"),
		type: encrypted.type
	};
}
function _encodeSignature(signature) {
	return {
		...signature,
		publicKey: toString(signature.publicKey, "base64"),
		signature: toString(signature.signature, "base64")
	};
}
function encodeChallengeRequest(msg) {
	return {
		...msg,
		challengeRequestId: _encodeChallengeRequestId(msg.challengeRequestId),
		encrypted: _encodeEncrypted(msg.encrypted),
		signature: _encodeSignature(msg.signature)
	};
}
function encodeChallengeMessage(msg) {
	return {
		...msg,
		challengeRequestId: _encodeChallengeRequestId(msg.challengeRequestId),
		encrypted: _encodeEncrypted(msg.encrypted),
		signature: _encodeSignature(msg.signature)
	};
}
function encodeChallengeAnswerMessage(msg) {
	return {
		...msg,
		challengeRequestId: _encodeChallengeRequestId(msg.challengeRequestId),
		encrypted: _encodeEncrypted(msg.encrypted),
		signature: _encodeSignature(msg.signature)
	};
}
function encodeChallengeVerificationMessage(msg) {
	const encrypted = msg.encrypted ? _encodeEncrypted(msg.encrypted) : void 0;
	return {
		...msg,
		challengeRequestId: _encodeChallengeRequestId(msg.challengeRequestId),
		encrypted,
		signature: _encodeSignature(msg.signature)
	};
}
//#endregion
//#region dist/node/rpc/src/json-rpc-util.js
var import_commonjs = /* @__PURE__ */ __toESM(require_commonjs(), 1);
var import_lib = require_lib();
function sanitizeRpcNotificationResult(event, result) {
	if (event !== "error" || !result || typeof result !== "object") return result;
	const sanitizedResult = { ...result };
	if ("stack" in sanitizedResult) delete sanitizedResult.stack;
	if ("details" in sanitizedResult && sanitizedResult.details && typeof sanitizedResult.details === "object") {
		const details = { ...sanitizedResult.details };
		const nestedError = details.error;
		if (nestedError && typeof nestedError === "object") {
			details.error = { ...nestedError };
			if ("stack" in details.error) delete details.error.stack;
		}
		sanitizedResult.details = details;
	}
	return sanitizedResult;
}
//#endregion
//#region dist/node/rpc/src/index.js
const log = logger_default("pkc-js-rpc:pkc-ws-server");
var PKCWsServer = class extends import_lib.TypedEmitter {
	constructor({ port, server, pkc, authKey, startStartedCommunitiesOnStartup, autoStartConcurrency, allowPrivateKeyExport, exportFileMaxAgeMs }) {
		super();
		this.connections = {};
		this.subscriptionCleanups = {};
		this.publishing = {};
		this._setSettingsQueue = Promise.resolve();
		this._trackedCommunityListeners = /* @__PURE__ */ new WeakMap();
		this._getIpFromConnectionRequest = (req) => req.socket.remoteAddress;
		this._onSettingsChange = {};
		this._startedCommunities = {};
		this._autoStartOnBoot = false;
		this._exportCommunityInstances = /* @__PURE__ */ new Map();
		this._ownsHttpServer = false;
		const log = logger_default("pkc-js:PKCWsServer");
		this.authKey = authKey;
		this._autoStartOnBoot = startStartedCommunitiesOnStartup ?? true;
		this._autoStartConcurrency = Math.max(1, autoStartConcurrency ?? 5);
		this._allowPrivateKeyExport = allowPrivateKeyExport ?? true;
		this._exportFileMaxAgeMs = exportFileMaxAgeMs ?? 1440 * 60 * 1e3;
		this._initPKC(pkc);
		if (server) this._httpServer = server;
		else {
			this._httpServer = http.createServer();
			this._ownsHttpServer = true;
			if (typeof port === "number") this._httpServer.listen(port);
		}
		this._httpServer.on("request", (req, res) => {
			this._handleExportsHttpRequest(req, res).catch((e) => {
				log.error("Unhandled error in /exports HTTP handler", e);
				if (!res.headersSent) {
					res.statusCode = 500;
					res.end();
				}
			});
		});
		this.rpcWebsockets = new Server({
			server: this._httpServer,
			verifyClient: ({ req }, callback) => {
				const requestOriginatorIp = this._getIpFromConnectionRequest(req);
				log.trace("Received new connection request from", requestOriginatorIp, "with url", req.url);
				const xForwardedFor = Boolean(req.rawHeaders.find((item, i) => item.toLowerCase() === "x-forwarded-for" && i % 2 === 0));
				const isLocalhost = req.socket.localAddress && req.socket.localAddress === requestOriginatorIp && !xForwardedFor;
				const hasAuth = this.authKey && `/${this.authKey}` === req.url;
				if (!isLocalhost && !hasAuth) {
					log(`Rejecting RPC connection request from`, requestOriginatorIp, `rejected because there is no auth key, url:`, req.url);
					callback(false, 403, "You need to set the auth key to connect remotely");
				} else callback(true);
			}
		});
		this.ws = this.rpcWebsockets.wss;
		this.rpcWebsockets.on("error", (error) => {
			log.error("RPC server", "Received an error on rpc-websockets", error);
			this._emitError(error);
		});
		this.ws.on("connection", (ws) => {
			this.connections[ws._id] = ws;
			this.subscriptionCleanups[ws._id] = {};
			this._onSettingsChange[ws._id] = {};
			log("Established connection with new RPC client", ws._id);
		});
		this.rpcWebsockets.on("disconnection", async (ws) => {
			log("RPC client disconnected", ws._id, "number of rpc clients connected", this.rpcWebsockets.wss.clients.size);
			const subscriptionCleanups = this.subscriptionCleanups[ws._id];
			if (!subscriptionCleanups) {
				delete this.subscriptionCleanups[ws._id];
				delete this.connections[ws._id];
				delete this._onSettingsChange[ws._id];
				log("Disconnected from RPC client (no subscriptions to clean)", ws._id);
				return;
			}
			for (const subscriptionId in subscriptionCleanups) {
				await subscriptionCleanups[subscriptionId]();
				delete subscriptionCleanups[subscriptionId];
			}
			delete this.subscriptionCleanups[ws._id];
			delete this.connections[ws._id];
			delete this._onSettingsChange[ws._id];
			log("Disconnected from RPC client", ws._id);
		});
		this.rpcWebsocketsRegister("getComment", this.getComment.bind(this));
		this.rpcWebsocketsRegister("getCommunityPage", this.getCommunityPage.bind(this));
		this.rpcWebsocketsRegister("getCommentPage", this.getCommentPage.bind(this));
		this.rpcWebsocketsRegister("createCommunity", this.createCommunity.bind(this));
		this.rpcWebsocketsRegister("startCommunity", this.startCommunity.bind(this));
		this.rpcWebsocketsRegister("stopCommunity", this.stopCommunity.bind(this));
		this.rpcWebsocketsRegister("editCommunity", this.editCommunity.bind(this));
		this.rpcWebsocketsRegister("deleteCommunity", this.deleteCommunity.bind(this));
		this.rpcWebsocketsRegister("communitiesSubscribe", this.communitiesSubscribe.bind(this));
		this.rpcWebsocketsRegister("settingsSubscribe", this.settingsSubscribe.bind(this));
		this.rpcWebsocketsRegister("fetchCid", this.fetchCid.bind(this));
		this.rpcWebsocketsRegister("resolveAuthorName", this.resolveAuthorName.bind(this));
		this.rpcWebsocketsRegister("setSettings", this.setSettings.bind(this));
		this.rpcWebsocketsRegister("commentUpdateSubscribe", this.commentUpdateSubscribe.bind(this));
		this.rpcWebsocketsRegister("communityUpdateSubscribe", this.communityUpdateSubscribe.bind(this));
		this.rpcWebsocketsRegister("publishComment", this.publishComment.bind(this));
		this.rpcWebsocketsRegister("publishCommunityEdit", this.publishCommunityEdit.bind(this));
		this.rpcWebsocketsRegister("publishVote", this.publishVote.bind(this));
		this.rpcWebsocketsRegister("publishCommentEdit", this.publishCommentEdit.bind(this));
		this.rpcWebsocketsRegister("publishCommentModeration", this.publishCommentModeration.bind(this));
		this.rpcWebsocketsRegister("publishChallengeAnswers", this.publishChallengeAnswers.bind(this));
		this.rpcWebsocketsRegister("unsubscribe", this.unsubscribe.bind(this));
		this.rpcWebsocketsRegister("exportCommunity", this.exportCommunity.bind(this));
		this.rpcWebsocketsRegister("exportsSubscribe", this.exportsSubscribe.bind(this));
		this.rpcWebsocketsRegister("cancelExport", this.cancelExport.bind(this));
		this.rpcWebsocketsRegister("exportCommunityModLogs", this.exportCommunityModLogs.bind(this));
		hideClassPrivateProps(this);
	}
	async getStartedCommunity(address) {
		if (!(address in this._startedCommunities)) throw Error("Can't call getStartedCommunity when the community hasn't been started");
		while (this._startedCommunities[address] === "pending") await new Promise((r) => setTimeout(r, 20));
		return this._startedCommunities[address];
	}
	_findCommunityAddress(identifier) {
		const { name, publicKey } = identifier;
		if (!name && !publicKey) throw new Error("At least one of name or publicKey must be provided");
		if (name && name in this._startedCommunities) return name;
		if (name && this.pkc.communities.includes(name)) return name;
		if (publicKey && publicKey in this._startedCommunities) return publicKey;
		if (publicKey && this.pkc.communities.includes(publicKey)) return publicKey;
	}
	_emitError(error) {
		if (this.listeners("error").length === 0) log.error("Unhandled error. This may crash your process, you need to listen for error event on PKCRpcWsServer", error);
		this.emit("error", error);
	}
	_getRpcStateDb() {
		if (this._rpcStateDb) return this._rpcStateDb;
		const dataPath = this.pkc.dataPath;
		if (!dataPath) return void 0;
		const rpcServerDir = path.join(dataPath, "rpc-server");
		mkdirSync(rpcServerDir, { recursive: true });
		const dbPath = path.join(rpcServerDir, "rpc-state.db");
		this._rpcStateDb = new Database(dbPath);
		this._rpcStateDb.pragma("journal_mode = WAL");
		try {
			this._rpcStateDb.exec("ALTER TABLE community_states RENAME TO community_states");
		} catch (_) {}
		this._rpcStateDb.exec(`
            CREATE TABLE IF NOT EXISTS community_states (
                address TEXT PRIMARY KEY,
                wasStarted INTEGER NOT NULL DEFAULT 0,
                wasExplicitlyStopped INTEGER NOT NULL DEFAULT 0
            )
        `);
		return this._rpcStateDb;
	}
	_updateCommunityState(address, update) {
		const db = this._getRpcStateDb();
		if (!db) return;
		db.prepare("INSERT OR IGNORE INTO community_states (address) VALUES (?)").run(address);
		if (update.wasStarted !== void 0) db.prepare("UPDATE community_states SET wasStarted = ? WHERE address = ?").run(update.wasStarted ? 1 : 0, address);
		if (update.wasExplicitlyStopped !== void 0) db.prepare("UPDATE community_states SET wasExplicitlyStopped = ? WHERE address = ?").run(update.wasExplicitlyStopped ? 1 : 0, address);
	}
	_removeCommunityState(address) {
		const db = this._getRpcStateDb();
		if (!db) return;
		db.prepare("DELETE FROM community_states WHERE address = ?").run(address);
	}
	async _autoStartPreviousCommunities() {
		if (!this._autoStartOnBoot) return;
		const autoStartLog = logger_default("pkc-js-rpc:pkc-ws-server:auto-start");
		autoStartLog("Checking for previously started communities to auto-start");
		const db = this._getRpcStateDb();
		if (!db) return;
		const rows = db.prepare("SELECT address FROM community_states WHERE wasStarted = 1 AND wasExplicitlyStopped = 0").all();
		const localCommunities = (await this._getPKCInstance()).communities;
		const communitiesToStart = [];
		for (const row of rows) {
			if (!localCommunities.includes(row.address)) {
				autoStartLog(`Skipping auto-start for ${row.address} - community no longer exists`);
				this._removeCommunityState(row.address);
				continue;
			}
			if (row.address in this._startedCommunities) {
				autoStartLog(`Skipping auto-start for ${row.address} - already started`);
				continue;
			}
			communitiesToStart.push(row.address);
		}
		if (communitiesToStart.length === 0) {
			autoStartLog("No communities to auto-start");
			return;
		}
		autoStartLog(`Auto-starting ${communitiesToStart.length} communities with concurrency limit of ${this._autoStartConcurrency}`);
		const limit = pLimit(this._autoStartConcurrency);
		await Promise.allSettled(communitiesToStart.map((address) => limit(async () => {
			autoStartLog(`Auto-starting community: ${address}`);
			try {
				await this._internalStartCommunity(address);
				autoStartLog(`Successfully auto-started: ${address}`);
			} catch (e) {
				autoStartLog.error(`Failed to auto-start community ${address}`, e);
				this._emitError(e instanceof Error ? e : /* @__PURE__ */ new Error(`Failed to auto-start ${address}: ${String(e)}`));
			}
		})));
	}
	async _internalStartCommunity(address) {
		const pkc = await this._getPKCInstance();
		this._startedCommunities[address] = "pending";
		try {
			const community = await pkc.createCommunity({ address });
			community.started = true;
			await community.start();
			this._startedCommunities[address] = community;
			this._updateCommunityState(address, {
				wasStarted: true,
				wasExplicitlyStopped: false
			});
			return community;
		} catch (e) {
			delete this._startedCommunities[address];
			throw e;
		}
	}
	rpcWebsocketsRegister(method, callback) {
		const callbackWithErrorHandled = async (params, connectionId) => {
			try {
				return await callback(params, connectionId);
			} catch (e) {
				const typedError = e;
				log.error(`${callback.name} error`, {
					params,
					error: typedError
				});
				if (typedError instanceof PKCError) {
					const seen = /* @__PURE__ */ new WeakSet();
					const errorJson = JSON.parse(JSON.stringify(typedError, (_key, value) => {
						if (typeof value === "object" && value !== null) {
							if (seen.has(value)) return void 0;
							seen.add(value);
						}
						return value;
					}));
					delete errorJson["stack"];
					throw errorJson;
				} else {
					const errorJson = {
						message: typedError.message,
						name: typedError.name
					};
					if ("details" in typedError) errorJson.details = typedError.details;
					if ("code" in typedError) errorJson.code = typedError.code;
					throw errorJson;
				}
			}
		};
		this.rpcWebsockets.register(method, callbackWithErrorHandled);
		if (this.authKey) this.rpcWebsockets.register(method, callbackWithErrorHandled, `/${this.authKey}`);
	}
	jsonRpcSendNotification({ method, result, subscription, event, connectionId }) {
		const message = {
			jsonrpc: "2.0",
			method,
			params: {
				result: sanitizeRpcNotificationResult(event, result),
				subscription,
				event
			}
		};
		this.connections[connectionId]?.send?.(JSON.stringify(message));
	}
	_registerPublishing(subscriptionId, publication, pkc, connectionId) {
		this.publishing[subscriptionId] = {
			publication,
			pkc,
			connectionId
		};
	}
	_clearPublishing(subscriptionId) {
		const record = this.publishing[subscriptionId];
		if (record?.timeout) clearTimeout(record.timeout);
		delete this.publishing[subscriptionId];
	}
	async _forceCleanupPublication(subscriptionId, reason) {
		const record = this.publishing[subscriptionId];
		if (!record) return;
		const cleanup = await this.subscriptionCleanups?.[record.connectionId]?.[subscriptionId];
		log(`Force-cleaning publication ${subscriptionId} after ${reason}`);
		if (cleanup) {
			await cleanup();
			if (this.subscriptionCleanups?.[record.connectionId]) delete this.subscriptionCleanups[record.connectionId][subscriptionId];
		}
		this._clearPublishing(subscriptionId);
		await this._retirePKCIfNeeded(record.pkc);
	}
	async _retirePKCIfNeeded(pkc) {
		if (Object.values(this.publishing).filter(({ pkc: p }) => p === pkc).length === 0 && !pkc.destroyed) {
			await pkc.destroy().catch((error) => log.error("Failed destroying old pkc immediately after setSettings", { error }));
			return;
		}
	}
	async _getPKCInstance() {
		await this._setSettingsQueue;
		return this.pkc;
	}
	async getComment(params) {
		const getCommentArgs = parseRpcCidParam(params[0]);
		return (await (await this._getPKCInstance()).getComment(getCommentArgs)).raw.comment;
	}
	async getCommunityPage(params) {
		const { cid: pageCid, communityPublicKey, communityName, type, pageMaxSize } = parseRpcCommunityPageParam(params[0]);
		if (!communityPublicKey && !communityName) throw Error("At least one of communityPublicKey or communityName must be provided");
		const communityAddress = communityName || communityPublicKey;
		const pkc = await this._getPKCInstance();
		const community = communityAddress in this._startedCommunities ? await this.getStartedCommunity(communityAddress) : await pkc.createCommunity({
			name: communityName,
			publicKey: communityPublicKey
		});
		const { page } = type === "posts" ? await community.posts._fetchAndVerifyPage({
			pageCid,
			pageMaxSize
		}) : await community.modQueue._fetchAndVerifyPage({
			pageCid,
			pageMaxSize
		});
		return {
			page,
			runtimeFields: buildPageRuntimeFields(page, pkc._memCaches.nameResolvedCache)
		};
	}
	async getCommentPage(params) {
		const { cid: pageCid, commentCid, communityPublicKey, communityName, pageMaxSize } = parseRpcCommentRepliesPageParam(params[0]);
		const pkc = await this._getPKCInstance();
		const { page } = await (await pkc.createComment({
			cid: commentCid,
			communityPublicKey,
			communityName
		})).replies._fetchAndVerifyPage({
			pageCid,
			pageMaxSize
		});
		return {
			page,
			runtimeFields: buildPageRuntimeFields(page, pkc._memCaches.nameResolvedCache)
		};
	}
	async createCommunity(params) {
		const createCommunityOptions = parseCreateNewLocalCommunityUserOptionsSchemaWithPKCErrorIfItFails(params[0]);
		const community = await (await this._getPKCInstance()).createCommunity(createCommunityOptions);
		if (!(community instanceof LocalCommunity)) throw Error("Failed to create a local community. This is a critical error");
		return community.toJSONInternalRpcBeforeFirstUpdate();
	}
	_trackCommunityListener(community, event, listener) {
		let listenersByEvent = this._trackedCommunityListeners.get(community);
		if (!listenersByEvent) {
			listenersByEvent = /* @__PURE__ */ new Map();
			this._trackedCommunityListeners.set(community, listenersByEvent);
		}
		let listeners = listenersByEvent.get(event);
		if (!listeners) {
			listeners = /* @__PURE__ */ new Set();
			listenersByEvent.set(event, listeners);
		}
		listeners.add(listener);
	}
	_untrackCommunityListener(community, event, listener) {
		const listenersByEvent = this._trackedCommunityListeners.get(community);
		if (!listenersByEvent) return;
		const listeners = listenersByEvent.get(event);
		if (!listeners) return;
		listeners.delete(listener);
		if (listeners.size === 0) listenersByEvent.delete(event);
		if (listenersByEvent.size === 0) this._trackedCommunityListeners.delete(community);
	}
	_setupStartedEvents(community, connectionId, subscriptionId) {
		const sendEvent = (event, result) => this.jsonRpcSendNotification({
			method: "startCommunity",
			subscription: subscriptionId,
			event,
			result,
			connectionId
		});
		const getUpdateJson = () => typeof community.updatedAt === "number" ? community.toJSONInternalRpcAfterFirstUpdate() : community.toJSONInternalRpcBeforeFirstUpdate();
		const updateListener = () => {
			const json = getUpdateJson();
			const communityIpfsRecord = community.raw.communityIpfs;
			if (communityIpfsRecord?.posts?.pages && "runtimeFields" in json && json.runtimeFields) Object.assign(json.runtimeFields, { posts: { pages: buildPagesRuntimeFields(communityIpfsRecord.posts.pages, community._pkc._memCaches.nameResolvedCache) } });
			sendEvent("update", json);
		};
		community.on("update", updateListener);
		this._trackCommunityListener(community, "update", updateListener);
		const startedStateListener = () => sendEvent("startedstatechange", { state: community.startedState });
		community.on("startedstatechange", startedStateListener);
		this._trackCommunityListener(community, "startedstatechange", startedStateListener);
		const requestListener = (request) => sendEvent("challengerequest", encodeChallengeRequest(request));
		community.on("challengerequest", requestListener);
		this._trackCommunityListener(community, "challengerequest", requestListener);
		const challengeListener = (challenge) => sendEvent("challenge", encodeChallengeMessage(challenge));
		community.on("challenge", challengeListener);
		this._trackCommunityListener(community, "challenge", challengeListener);
		const challengeAnswerListener = (answer) => sendEvent("challengeanswer", encodeChallengeAnswerMessage(answer));
		community.on("challengeanswer", challengeAnswerListener);
		this._trackCommunityListener(community, "challengeanswer", challengeAnswerListener);
		const challengeVerificationListener = (challengeVerification) => sendEvent("challengeverification", { challengeVerification: encodeChallengeVerificationMessage(challengeVerification) });
		community.on("challengeverification", challengeVerificationListener);
		this._trackCommunityListener(community, "challengeverification", challengeVerificationListener);
		const errorListener = (error) => {
			const rpcError = error;
			if (community.state === "started") rpcError.details = {
				...rpcError.details,
				newStartedState: community.startedState
			};
			else if (community.state === "updating") rpcError.details = {
				...rpcError.details,
				newUpdatingState: community.updatingState
			};
			log("community rpc error", rpcError);
			sendEvent("error", rpcError);
		};
		community.on("error", errorListener);
		this._trackCommunityListener(community, "error", errorListener);
		this.subscriptionCleanups[connectionId][subscriptionId] = async () => {
			community.removeListener("update", updateListener);
			this._untrackCommunityListener(community, "update", updateListener);
			community.removeListener("startedstatechange", startedStateListener);
			this._untrackCommunityListener(community, "startedstatechange", startedStateListener);
			community.removeListener("challengerequest", requestListener);
			this._untrackCommunityListener(community, "challengerequest", requestListener);
			community.removeListener("challenge", challengeListener);
			this._untrackCommunityListener(community, "challenge", challengeListener);
			community.removeListener("challengeanswer", challengeAnswerListener);
			this._untrackCommunityListener(community, "challengeanswer", challengeAnswerListener);
			community.removeListener("challengeverification", challengeVerificationListener);
			this._untrackCommunityListener(community, "challengeverification", challengeVerificationListener);
			community.removeListener("error", errorListener);
			this._untrackCommunityListener(community, "error", errorListener);
			if (this._onSettingsChange[connectionId]) delete this._onSettingsChange[connectionId][subscriptionId];
		};
	}
	async startCommunity(params, connectionId) {
		const { name, publicKey } = parseRpcCommunityIdentifierParam(params[0]);
		await this._getPKCInstance();
		const address = this._findCommunityAddress({
			name,
			publicKey
		});
		if (!address) throw new PKCError("ERR_RPC_CLIENT_ATTEMPTING_TO_START_A_REMOTE_COMMUNITY", { communityAddress: name || publicKey });
		const subscriptionId = generateSubscriptionId();
		const startCommunityImpl = async () => {
			const pkc = await this._getPKCInstance();
			if (address in this._startedCommunities) {
				const community = await this.getStartedCommunity(address);
				this._setupStartedEvents(community, connectionId, subscriptionId);
			} else try {
				this._startedCommunities[address] = "pending";
				const community = await pkc.createCommunity({ address });
				this._setupStartedEvents(community, connectionId, subscriptionId);
				community.started = true;
				community.emit("update", community);
				await community.start();
				this._startedCommunities[address] = community;
				this._updateCommunityState(address, {
					wasStarted: true,
					wasExplicitlyStopped: false
				});
			} catch (e) {
				const cleanup = this.subscriptionCleanups?.[connectionId]?.[subscriptionId];
				if (cleanup) await cleanup();
				delete this._startedCommunities[address];
				throw e;
			}
		};
		this._onSettingsChange[connectionId][subscriptionId] = async ({ newPKC }) => {
			const current = this._startedCommunities[address];
			if (!current || current === "pending") return;
			const community = await this.getStartedCommunity(address);
			this._startedCommunities[address] = "pending";
			try {
				await community.stop();
				community._pkc = newPKC;
				await community.start();
				this._startedCommunities[address] = community;
			} catch (error) {
				delete this._startedCommunities[address];
				throw error;
			}
		};
		await startCommunityImpl();
		return { subscriptionId };
	}
	async stopCommunity(params) {
		const { name, publicKey } = parseRpcCommunityIdentifierParam(params[0]);
		await this._getPKCInstance();
		const address = this._findCommunityAddress({
			name,
			publicKey
		});
		if (!address) throw new PKCError("ERR_RPC_CLIENT_TRYING_TO_STOP_REMOTE_COMMUNITY", { communityAddress: name || publicKey });
		if (!(address in this._startedCommunities)) throw new PKCError("ERR_RPC_CLIENT_TRYING_TO_STOP_COMMUNITY_THAT_IS_NOT_RUNNING", { communityAddress: address });
		const startedCommunity = await this.getStartedCommunity(address);
		await startedCommunity.stop();
		await this._postStoppingOrDeleting(startedCommunity);
		delete this._startedCommunities[address];
		this._updateCommunityState(address, { wasExplicitlyStopped: true });
		return { success: true };
	}
	async _postStoppingOrDeleting(community) {
		community.emit("update", community);
		community.emit("startedstatechange", community.startedState);
		const trackedListeners = this._trackedCommunityListeners.get(community);
		if (trackedListeners) {
			for (const [event, listeners] of trackedListeners) for (const listener of listeners) community.removeListener(event, listener);
			this._trackedCommunityListeners.delete(community);
		}
	}
	async editCommunity(params) {
		const rawParam = params[0];
		rawParam.editOptions = replaceXWithY(rawParam.editOptions, null, void 0);
		const { name, publicKey, editOptions } = parseRpcEditCommunityParam(rawParam);
		const editCommunityOptions = parseCommunityEditOptionsSchemaWithPKCErrorIfItFails(editOptions);
		const pkc = await this._getPKCInstance();
		const address = this._findCommunityAddress({
			name,
			publicKey
		});
		if (!address) throw new PKCError("ERR_RPC_CLIENT_TRYING_TO_EDIT_REMOTE_COMMUNITY", { communityAddress: name || publicKey });
		let community;
		if (this._startedCommunities[address] instanceof LocalCommunity) community = this._startedCommunities[address];
		else {
			community = await pkc.createCommunity({ address });
			community.once("error", (error) => {
				log.error("RPC server Received an error on community", community.address, "edit", error);
			});
		}
		await community.edit(editCommunityOptions);
		if (editCommunityOptions.address && this._startedCommunities[address]) {
			this._startedCommunities[editCommunityOptions.address] = this._startedCommunities[address];
			delete this._startedCommunities[address];
			const db = this._getRpcStateDb();
			if (db) db.prepare("UPDATE community_states SET address = @newAddress WHERE address = @oldAddress").run({
				newAddress: editCommunityOptions.address,
				oldAddress: address
			});
		}
		if (typeof community.updatedAt === "number") return community.toJSONInternalRpcAfterFirstUpdate();
		else return community.toJSONInternalRpcBeforeFirstUpdate();
	}
	async deleteCommunity(params) {
		const { name, publicKey } = parseRpcCommunityIdentifierParam(params[0]);
		const pkc = await this._getPKCInstance();
		const address = this._findCommunityAddress({
			name,
			publicKey
		});
		if (!address) throw new PKCError("ERR_RPC_CLIENT_TRYING_TO_DELETE_REMOTE_COMMUNITY", { communityAddress: name || publicKey });
		const community = address in this._startedCommunities ? await this.getStartedCommunity(address) : await pkc.createCommunity({ address });
		await community.delete();
		await this._postStoppingOrDeleting(community);
		delete this._startedCommunities[address];
		this._removeCommunityState(address);
		return { success: true };
	}
	async communitiesSubscribe(params, connectionId) {
		const subscriptionId = generateSubscriptionId();
		const sendEvent = (event, result) => {
			this.jsonRpcSendNotification({
				method: "communitiesNotification",
				subscription: Number(subscriptionId),
				event,
				result,
				connectionId
			});
		};
		const pkcSubscribeEvent = (newCommunities) => sendEvent("communitieschange", { communities: newCommunities });
		const pkc = await this._getPKCInstance();
		pkc.on("communitieschange", pkcSubscribeEvent);
		this.subscriptionCleanups[connectionId][subscriptionId] = async () => {
			pkc.removeListener("communitieschange", pkcSubscribeEvent);
		};
		sendEvent("communitieschange", { communities: pkc.communities });
		return { subscriptionId };
	}
	async fetchCid(params) {
		const parsedArgs = parseRpcCidParam(params[0]);
		return (await this._getPKCInstance()).fetchCid(parsedArgs);
	}
	_serializeSettingsFromPKC(pkc) {
		const pkcOptions = pkc.parsedPKCOptions;
		const allChallengeFactories = {
			...PKCJs.PKC.challenges || {},
			...pkc.settings?.challenges || {}
		};
		return {
			pkcOptions,
			challenges: import_commonjs.mapValues(allChallengeFactories, (challengeFactory) => import_commonjs.omit(challengeFactory({ challengeSettings: {} }), ["getChallenge"]))
		};
	}
	async settingsSubscribe(params, connectionId) {
		const subscriptionId = generateSubscriptionId();
		const sendEvent = (event, result) => {
			this.jsonRpcSendNotification({
				method: "settingsNotification",
				subscription: Number(subscriptionId),
				event,
				result,
				connectionId
			});
		};
		const sendRpcSettings = async ({ newPKC }) => {
			sendEvent("settingschange", this._serializeSettingsFromPKC(newPKC));
		};
		this.subscriptionCleanups[connectionId][subscriptionId] = async () => {
			if (this._onSettingsChange[connectionId]) delete this._onSettingsChange[connectionId][subscriptionId];
		};
		this._onSettingsChange[connectionId][subscriptionId] = sendRpcSettings;
		await sendRpcSettings({ newPKC: this.pkc });
		return { subscriptionId };
	}
	_initPKC(pkc) {
		this.pkc = pkc;
		pkc.on("error", (error) => log.error("RPC server", "Received an error on pkc instance", error));
	}
	async _createPKCInstanceFromSetSettings(newOptions) {
		return PKCJs.PKC(newOptions);
	}
	async setSettings(params) {
		const runSetSettings = async () => {
			const settings = parseSetNewSettingsPKCWsServerSchemaWithPKCErrorIfItFails(params[0]);
			const currentSettings = this._serializeSettingsFromPKC(this.pkc);
			if (stringify(settings.pkcOptions) === stringify(currentSettings.pkcOptions)) {
				log("RPC client called setSettings with the same settings as the current one, aborting");
				return;
			}
			log(`RPC client called setSettings, the clients need to call all subscription methods again`);
			const oldPKC = this.pkc;
			const { nameResolvers: _stripNr, ...pkcOptionsWithoutNameResolvers } = settings.pkcOptions;
			const newPKC = await this._createPKCInstanceFromSetSettings({
				...pkcOptionsWithoutNameResolvers,
				nameResolvers: this.pkc.parsedPKCOptions.nameResolvers
			});
			this._initPKC(newPKC);
			for (const connectionId of import_commonjs.keys.strict(this._onSettingsChange)) {
				const connectionHandlers = this._onSettingsChange[connectionId];
				if (!connectionHandlers) continue;
				for (const subscriptionId of import_commonjs.keys.strict(connectionHandlers)) {
					const handler = connectionHandlers[subscriptionId];
					if (!handler) continue;
					try {
						await handler({ newPKC });
					} catch (error) {
						log.error(`Failed to apply settings change to subscription ${subscriptionId} of connection ${connectionId}, continuing with remaining subscriptions`, error);
					}
				}
			}
			for (const [subscriptionId, pub] of Object.entries(this.publishing).filter((pub) => pub[1].pkc === oldPKC)) pub.timeout = setTimeout(async () => {
				await this._forceCleanupPublication(Number(subscriptionId), "timeout");
			}, 6e4);
			setTimeout(async () => {
				await this._retirePKCIfNeeded(oldPKC);
			}, 6e4);
		};
		const setSettingsRun = this._setSettingsQueue.then(() => runSetSettings());
		this._setSettingsQueue = setSettingsRun.catch(() => {});
		await setSettingsRun;
		return { success: true };
	}
	async commentUpdateSubscribe(params, connectionId) {
		const logUpdate = logger_default("pkc-js-rpc:pkc-ws-server:commentUpdateSubscribe");
		const parsedCommentUpdateArgs = parseRpcCidParam(params[0]);
		const subscriptionId = generateSubscriptionId();
		const sendEvent = (event, result) => this.jsonRpcSendNotification({
			method: "commentUpdateNotification",
			subscription: subscriptionId,
			event,
			result,
			connectionId
		});
		let sentCommentIpfsUpdateEvent = false;
		let lastSentNameResolved = void 0;
		const pkc = await this._getPKCInstance();
		const comment = await pkc.createComment(parsedCommentUpdateArgs);
		const sendUpdate = () => {
			if (!sentCommentIpfsUpdateEvent && comment.raw.comment) {
				sendEvent("comment", {
					comment: comment.raw.comment,
					runtimeFields: { author: { nameResolved: comment.author.nameResolved } }
				});
				sentCommentIpfsUpdateEvent = true;
				lastSentNameResolved = comment.author.nameResolved;
			}
			if (comment.raw.commentUpdate) {
				const updateEvent = { commentUpdate: comment.raw.commentUpdate };
				const runtimeFields = {};
				if (comment.raw.commentUpdate.replies?.pages) runtimeFields.replies = { pages: buildPagesRuntimeFields(comment.raw.commentUpdate.replies.pages, pkc._memCaches.nameResolvedCache) };
				if (typeof comment.author.nameResolved === "boolean") runtimeFields.author = { nameResolved: comment.author.nameResolved };
				if (Object.keys(runtimeFields).length > 0) updateEvent.runtimeFields = runtimeFields;
				sendEvent("update", updateEvent);
				lastSentNameResolved = comment.author.nameResolved;
			} else if (sentCommentIpfsUpdateEvent && typeof comment.author.nameResolved === "boolean" && comment.author.nameResolved !== lastSentNameResolved) {
				sendEvent("runtimeupdate", { author: { nameResolved: comment.author.nameResolved } });
				lastSentNameResolved = comment.author.nameResolved;
			}
		};
		const updateListener = () => sendUpdate();
		comment.on("update", updateListener);
		const updatingStateListener = () => sendEvent("updatingstatechange", { state: comment.updatingState });
		comment.on("updatingstatechange", updatingStateListener);
		const stateListener = () => sendEvent("statechange", { state: comment.state });
		comment.on("statechange", stateListener);
		const errorListener = (error) => {
			const errorWithNewUpdatingState = error;
			if (comment.state === "publishing") errorWithNewUpdatingState.details = {
				...errorWithNewUpdatingState.details,
				newPublishingState: comment.publishingState
			};
			else if (comment.state === "updating") errorWithNewUpdatingState.details = {
				...errorWithNewUpdatingState.details,
				newUpdatingState: comment.updatingState
			};
			sendEvent("error", errorWithNewUpdatingState);
		};
		comment.on("error", errorListener);
		this.subscriptionCleanups[connectionId][subscriptionId] = async () => {
			logUpdate("Cleaning up commentUpdate subscription", {
				subscriptionId,
				connectionId,
				cid: comment.cid
			});
			comment.removeListener("update", updateListener);
			comment.removeListener("updatingstatechange", updatingStateListener);
			comment.removeListener("statechange", stateListener);
			comment.removeListener("error", errorListener);
			await comment.stop();
			if (this._onSettingsChange[connectionId]) delete this._onSettingsChange[connectionId][subscriptionId];
		};
		this._onSettingsChange[connectionId][subscriptionId] = async ({ newPKC }) => {
			comment._pkc = newPKC;
			await comment.update();
		};
		try {
			sendUpdate();
			await comment.update();
		} catch (e) {
			logUpdate.error("Cleaning up subscription to comment", comment.cid, "because comment.update threw an error", e);
			const cleanup = this.subscriptionCleanups?.[connectionId]?.[subscriptionId];
			if (cleanup) await cleanup();
			throw e;
		}
		return { subscriptionId };
	}
	async communityUpdateSubscribe(params, connectionId) {
		const parsedCommunityUpdateArgs = parseRpcCommunityIdentifierParam(params[0]);
		const subscriptionId = generateSubscriptionId();
		await this._bindCommunityUpdateSubscription(parsedCommunityUpdateArgs, connectionId, subscriptionId);
		return { subscriptionId };
	}
	async _bindCommunityUpdateSubscription(parsedArgs, connectionId, subscriptionId) {
		const sendEvent = (event, result) => this.jsonRpcSendNotification({
			method: "communityUpdateNotification",
			subscription: subscriptionId,
			event,
			result,
			connectionId
		});
		const pkc = await this._getPKCInstance();
		const startedCommunity = findStartedCommunity(pkc, parsedArgs);
		const isStartedCommunity = Boolean(startedCommunity);
		const community = startedCommunity || await pkc.createCommunity(parsedArgs);
		const sendCommunityJson = () => {
			let jsonToSend;
			if (community instanceof LocalCommunity) jsonToSend = typeof community.updatedAt === "number" ? community.toJSONInternalRpcAfterFirstUpdate() : community.toJSONInternalRpcBeforeFirstUpdate();
			else jsonToSend = community.toJSONRpcRemote();
			const communityIpfsRecord = community.raw.communityIpfs;
			if (communityIpfsRecord?.posts?.pages && "runtimeFields" in jsonToSend && jsonToSend.runtimeFields) Object.assign(jsonToSend.runtimeFields, { posts: { pages: buildPagesRuntimeFields(communityIpfsRecord.posts.pages, pkc._memCaches.nameResolvedCache) } });
			sendEvent("update", jsonToSend);
		};
		const updateListener = () => sendCommunityJson();
		community.on("update", updateListener);
		const updatingStateListener = () => sendEvent("updatingstatechange", { state: community.updatingState });
		community.on("updatingstatechange", updatingStateListener);
		const startedStateListener = () => sendEvent("updatingstatechange", { state: community.startedState });
		if (isStartedCommunity) community.on("startedstatechange", startedStateListener);
		const errorListener = (error) => {
			const rpcError = error;
			if (community.state === "started") rpcError.details = {
				...rpcError.details,
				newStartedState: community.startedState
			};
			else if (community.state === "updating") rpcError.details = {
				...rpcError.details,
				newUpdatingState: community.updatingState
			};
			log("community rpc error", rpcError);
			sendEvent("error", rpcError);
		};
		community.on("error", errorListener);
		this.subscriptionCleanups[connectionId][subscriptionId] = async () => {
			log("Cleaning up community", community.address, "client subscription");
			community.removeListener("update", updateListener);
			community.removeListener("updatingstatechange", updatingStateListener);
			community.removeListener("error", errorListener);
			community.removeListener("startedstatechange", startedStateListener);
			if (this._onSettingsChange[connectionId]) delete this._onSettingsChange[connectionId][subscriptionId];
			if (!isStartedCommunity && community.state !== "stopped") await community.stop();
		};
		this._onSettingsChange[connectionId][subscriptionId] = async ({ newPKC }) => {
			if (!isStartedCommunity) {
				community._pkc = newPKC;
				await community.stop();
				await community.update();
			}
		};
		try {
			if ("signer" in community || community.raw.communityIpfs) sendCommunityJson();
			if (!isStartedCommunity) await community.update();
		} catch (e) {
			const cleanup = this.subscriptionCleanups?.[connectionId]?.[subscriptionId];
			if (cleanup) await cleanup();
			throw e;
		}
	}
	async _createCommentInstanceFromPublishCommentParams(params) {
		const comment = await (await this._getPKCInstance()).createComment(params.comment);
		comment.challengeRequest = import_commonjs.omit(params, ["comment"]);
		return comment;
	}
	async publishComment(params, connectionId) {
		const publishOptions = parseCommentChallengeRequestToEncryptSchemaWithPKCErrorIfItFails(params[0]);
		const subscriptionId = generateSubscriptionId();
		const sendEvent = (event, result) => this.jsonRpcSendNotification({
			method: "publishCommentNotification",
			subscription: subscriptionId,
			event,
			result,
			connectionId
		});
		const comment = await this._createCommentInstanceFromPublishCommentParams(publishOptions);
		this._registerPublishing(subscriptionId, comment, comment._pkc, connectionId);
		const challengeListener = (challenge) => sendEvent("challenge", encodeChallengeMessage(challenge));
		comment.on("challenge", challengeListener);
		const challengeAnswerListener = (answer) => sendEvent("challengeanswer", encodeChallengeAnswerMessage(answer));
		comment.on("challengeanswer", challengeAnswerListener);
		const challengeRequestListener = (request) => sendEvent("challengerequest", encodeChallengeRequest(request));
		comment.on("challengerequest", challengeRequestListener);
		const challengeVerificationListener = (challengeVerification) => sendEvent("challengeverification", {
			challengeVerification: encodeChallengeVerificationMessage(challengeVerification),
			runtimeFields: { author: { nameResolved: comment.author.nameResolved } }
		});
		comment.on("challengeverification", challengeVerificationListener);
		const publishingStateListener = () => {
			sendEvent("publishingstatechange", { state: comment.publishingState });
		};
		comment.on("publishingstatechange", publishingStateListener);
		const stateListener = () => sendEvent("statechange", { state: comment.state });
		comment.on("statechange", stateListener);
		const errorListener = (error) => {
			const commentRpcError = error;
			commentRpcError.details = {
				...commentRpcError.details,
				newPublishingState: comment.publishingState
			};
			sendEvent("error", commentRpcError);
		};
		comment.on("error", errorListener);
		this.subscriptionCleanups[connectionId][subscriptionId] = async () => {
			comment.removeListener("challenge", challengeListener);
			comment.removeListener("challengeanswer", challengeAnswerListener);
			comment.removeListener("challengerequest", challengeRequestListener);
			comment.removeListener("challengeverification", challengeVerificationListener);
			comment.removeListener("publishingstatechange", publishingStateListener);
			comment.removeListener("statechange", stateListener);
			comment.removeListener("error", errorListener);
			await comment.stop();
			this._clearPublishing(subscriptionId);
			if (this._onSettingsChange[connectionId]) delete this._onSettingsChange[connectionId][subscriptionId];
		};
		try {
			await comment.publish();
		} catch (e) {
			const error = e;
			error.details = {
				...error.details,
				publishThrowError: true
			};
			errorListener(error);
			const cleanup = this.subscriptionCleanups?.[connectionId]?.[subscriptionId];
			if (cleanup) await cleanup();
			return { subscriptionId };
		}
		return { subscriptionId };
	}
	async _createVoteInstanceFromPublishVoteParams(params) {
		const vote = await (await this._getPKCInstance()).createVote(params.vote);
		vote.challengeRequest = import_commonjs.omit(params, ["vote"]);
		return vote;
	}
	async publishVote(params, connectionId) {
		const publishOptions = parseVoteChallengeRequestToEncryptSchemaWithPKCErrorIfItFails(params[0]);
		const subscriptionId = generateSubscriptionId();
		const sendEvent = (event, result) => this.jsonRpcSendNotification({
			method: "publishVoteNotification",
			subscription: subscriptionId,
			event,
			result,
			connectionId
		});
		const vote = await this._createVoteInstanceFromPublishVoteParams(publishOptions);
		this._registerPublishing(subscriptionId, vote, vote._pkc, connectionId);
		const challengeListener = (challenge) => sendEvent("challenge", encodeChallengeMessage(challenge));
		vote.on("challenge", challengeListener);
		const challengeAnswerListener = (answer) => sendEvent("challengeanswer", encodeChallengeAnswerMessage(answer));
		vote.on("challengeanswer", challengeAnswerListener);
		const challengeRequestListener = (request) => sendEvent("challengerequest", encodeChallengeRequest(request));
		vote.on("challengerequest", challengeRequestListener);
		const challengeVerificationListener = (challengeVerification) => sendEvent("challengeverification", { challengeVerification: encodeChallengeVerificationMessage(challengeVerification) });
		vote.on("challengeverification", challengeVerificationListener);
		const publishingStateListener = () => sendEvent("publishingstatechange", { state: vote.publishingState });
		vote.on("publishingstatechange", publishingStateListener);
		const errorListener = (error) => {
			const voteRpcError = error;
			voteRpcError.details = {
				...voteRpcError.details,
				newPublishingState: vote.publishingState
			};
			sendEvent("error", voteRpcError);
		};
		vote.on("error", errorListener);
		this.subscriptionCleanups[connectionId][subscriptionId] = async () => {
			this._clearPublishing(subscriptionId);
			await vote.stop();
			vote.removeListener("challenge", challengeListener);
			vote.removeListener("challengeanswer", challengeAnswerListener);
			vote.removeListener("challengerequest", challengeRequestListener);
			vote.removeListener("challengeverification", challengeVerificationListener);
			vote.removeListener("publishingstatechange", publishingStateListener);
			vote.removeListener("error", errorListener);
		};
		try {
			await vote.publish();
		} catch (e) {
			const error = e;
			error.details = {
				...error.details,
				publishThrowError: true
			};
			errorListener(error);
			const cleanup = this.subscriptionCleanups?.[connectionId]?.[subscriptionId];
			if (cleanup) await cleanup();
		}
		return { subscriptionId };
	}
	async _createCommunityEditInstanceFromPublishCommunityEditParams(params) {
		const communityEdit = await (await this._getPKCInstance()).createCommunityEdit(params.communityEdit);
		communityEdit.challengeRequest = import_commonjs.omit(params, ["communityEdit"]);
		return communityEdit;
	}
	async publishCommunityEdit(params, connectionId) {
		const publishOptions = parseCommunityEditChallengeRequestToEncryptSchemaWithPKCErrorIfItFails(params[0]);
		const subscriptionId = generateSubscriptionId();
		const sendEvent = (event, result) => this.jsonRpcSendNotification({
			method: "publishCommunityEditNotification",
			subscription: subscriptionId,
			event,
			result,
			connectionId
		});
		const communityEdit = await this._createCommunityEditInstanceFromPublishCommunityEditParams(publishOptions);
		this._registerPublishing(subscriptionId, communityEdit, communityEdit._pkc, connectionId);
		const challengeListener = (challenge) => sendEvent("challenge", encodeChallengeMessage(challenge));
		communityEdit.on("challenge", challengeListener);
		const challengeAnswerListener = (answer) => sendEvent("challengeanswer", encodeChallengeAnswerMessage(answer));
		communityEdit.on("challengeanswer", challengeAnswerListener);
		const challengeRequestListener = (request) => sendEvent("challengerequest", encodeChallengeRequest(request));
		communityEdit.on("challengerequest", challengeRequestListener);
		const challengeVerificationListener = (challengeVerification) => sendEvent("challengeverification", { challengeVerification: encodeChallengeVerificationMessage(challengeVerification) });
		communityEdit.on("challengeverification", challengeVerificationListener);
		const publishingStateListener = () => sendEvent("publishingstatechange", { state: communityEdit.publishingState });
		communityEdit.on("publishingstatechange", publishingStateListener);
		const errorListener = (error) => {
			const editRpcError = error;
			editRpcError.details = {
				...editRpcError.details,
				newPublishingState: communityEdit.publishingState
			};
			sendEvent("error", editRpcError);
		};
		communityEdit.on("error", errorListener);
		this.subscriptionCleanups[connectionId][subscriptionId] = async () => {
			this._clearPublishing(subscriptionId);
			await communityEdit.stop();
			communityEdit.removeListener("challenge", challengeListener);
			communityEdit.removeListener("challengeanswer", challengeAnswerListener);
			communityEdit.removeListener("challengerequest", challengeRequestListener);
			communityEdit.removeListener("challengeverification", challengeVerificationListener);
			communityEdit.removeListener("publishingstatechange", publishingStateListener);
			communityEdit.removeListener("error", errorListener);
		};
		try {
			await communityEdit.publish();
		} catch (e) {
			const error = e;
			error.details = {
				...error.details,
				publishThrowError: true
			};
			errorListener(error);
			const cleanup = this.subscriptionCleanups?.[connectionId]?.[subscriptionId];
			if (cleanup) await cleanup();
		}
		return { subscriptionId };
	}
	async _createCommentEditInstanceFromPublishCommentEditParams(params) {
		const commentEdit = await (await this._getPKCInstance()).createCommentEdit(params.commentEdit);
		commentEdit.challengeRequest = import_commonjs.omit(params, ["commentEdit"]);
		return commentEdit;
	}
	async publishCommentEdit(params, connectionId) {
		const publishOptions = parseCommentEditChallengeRequestToEncryptSchemaWithPKCErrorIfItFails(params[0]);
		const subscriptionId = generateSubscriptionId();
		const sendEvent = (event, result) => this.jsonRpcSendNotification({
			method: "publishCommentEditNotification",
			subscription: subscriptionId,
			event,
			result,
			connectionId
		});
		const commentEdit = await this._createCommentEditInstanceFromPublishCommentEditParams(publishOptions);
		this._registerPublishing(subscriptionId, commentEdit, commentEdit._pkc, connectionId);
		const challengeListener = (challenge) => sendEvent("challenge", encodeChallengeMessage(challenge));
		commentEdit.on("challenge", challengeListener);
		const challengeAnswerListener = (answer) => sendEvent("challengeanswer", encodeChallengeAnswerMessage(answer));
		commentEdit.on("challengeanswer", challengeAnswerListener);
		const challengeRequestListener = (request) => sendEvent("challengerequest", encodeChallengeRequest(request));
		commentEdit.on("challengerequest", challengeRequestListener);
		const challengeVerificationListener = (challengeVerification) => sendEvent("challengeverification", { challengeVerification: encodeChallengeVerificationMessage(challengeVerification) });
		commentEdit.on("challengeverification", challengeVerificationListener);
		const publishingStateListener = () => sendEvent("publishingstatechange", { state: commentEdit.publishingState });
		commentEdit.on("publishingstatechange", publishingStateListener);
		const errorListener = (error) => {
			const commentEditRpcError = error;
			commentEditRpcError.details = {
				...commentEditRpcError.details,
				newPublishingState: commentEdit.publishingState
			};
			sendEvent("error", commentEditRpcError);
		};
		commentEdit.on("error", errorListener);
		this.subscriptionCleanups[connectionId][subscriptionId] = async () => {
			this._clearPublishing(subscriptionId);
			await commentEdit.stop();
			commentEdit.removeListener("challenge", challengeListener);
			commentEdit.removeListener("challengeanswer", challengeAnswerListener);
			commentEdit.removeListener("challengerequest", challengeRequestListener);
			commentEdit.removeListener("challengeverification", challengeVerificationListener);
			commentEdit.removeListener("publishingstatechange", publishingStateListener);
			commentEdit.removeListener("error", errorListener);
		};
		try {
			await commentEdit.publish();
		} catch (e) {
			const error = e;
			error.details = {
				...error.details,
				publishThrowError: true
			};
			errorListener(error);
			const cleanup = this.subscriptionCleanups?.[connectionId]?.[subscriptionId];
			if (cleanup) await cleanup();
		}
		return { subscriptionId };
	}
	async _createCommentModerationInstanceFromPublishCommentModerationParams(params) {
		const commentModeration = await (await this._getPKCInstance()).createCommentModeration(params.commentModeration);
		commentModeration.challengeRequest = import_commonjs.omit(params, ["commentModeration"]);
		return commentModeration;
	}
	async publishCommentModeration(params, connectionId) {
		const publishOptions = parseCommentModerationChallengeRequestToEncryptSchemaWithPKCErrorIfItFails(params[0]);
		const subscriptionId = generateSubscriptionId();
		const sendEvent = (event, result) => this.jsonRpcSendNotification({
			method: "publishCommentModerationNotification",
			subscription: subscriptionId,
			event,
			result,
			connectionId
		});
		const commentMod = await this._createCommentModerationInstanceFromPublishCommentModerationParams(publishOptions);
		this._registerPublishing(subscriptionId, commentMod, commentMod._pkc, connectionId);
		const challengeListener = (challenge) => sendEvent("challenge", encodeChallengeMessage(challenge));
		commentMod.on("challenge", challengeListener);
		const challengeAnswerListener = (answer) => sendEvent("challengeanswer", encodeChallengeAnswerMessage(answer));
		commentMod.on("challengeanswer", challengeAnswerListener);
		const challengeRequestListener = (request) => sendEvent("challengerequest", encodeChallengeRequest(request));
		commentMod.on("challengerequest", challengeRequestListener);
		const challengeVerificationListener = (challengeVerification) => sendEvent("challengeverification", { challengeVerification: encodeChallengeVerificationMessage(challengeVerification) });
		commentMod.on("challengeverification", challengeVerificationListener);
		const publishingStateListener = () => sendEvent("publishingstatechange", { state: commentMod.publishingState });
		commentMod.on("publishingstatechange", publishingStateListener);
		const errorListener = (error) => {
			const commentModRpcError = error;
			commentModRpcError.details = {
				...commentModRpcError.details,
				newPublishingState: commentMod.publishingState
			};
			sendEvent("error", commentModRpcError);
		};
		commentMod.on("error", errorListener);
		this.subscriptionCleanups[connectionId][subscriptionId] = async () => {
			commentMod.removeListener("challenge", challengeListener);
			commentMod.removeListener("challengeanswer", challengeAnswerListener);
			commentMod.removeListener("challengerequest", challengeRequestListener);
			commentMod.removeListener("challengeverification", challengeVerificationListener);
			commentMod.removeListener("publishingstatechange", publishingStateListener);
			commentMod.removeListener("error", errorListener);
			await commentMod.stop();
			this._clearPublishing(subscriptionId);
		};
		try {
			await commentMod.publish();
		} catch (e) {
			const error = e;
			error.details = {
				...error.details,
				publishThrowError: true
			};
			errorListener(error);
			const cleanup = this.subscriptionCleanups?.[connectionId]?.[subscriptionId];
			if (cleanup) await cleanup();
		}
		return { subscriptionId };
	}
	async publishChallengeAnswers(params) {
		const parsed = parseRpcPublishChallengeAnswersParam(params[0]);
		const subscriptionId = parsed.subscriptionId;
		const decryptedChallengeAnswers = parseDecryptedChallengeAnswerWithPKCErrorIfItFails({ challengeAnswers: parsed.challengeAnswers });
		const record = this.publishing[subscriptionId];
		if (!record?.publication) throw Error(`no subscription with id '${subscriptionId}'`);
		const publication = record.publication;
		await this._getPKCInstance();
		await publication.publishChallengeAnswers({ challengeAnswers: decryptedChallengeAnswers.challengeAnswers });
		return { success: true };
	}
	async resolveAuthorName(params) {
		const parsedArgs = parseRpcAuthorNameParam(params[0]);
		return (await this._getPKCInstance()).resolveAuthorName(parsedArgs);
	}
	async _resolveLocalCommunityForExport(parsedArgs) {
		const address = this._findCommunityAddress(parsedArgs);
		if (!address) throw new PKCError("ERR_COMMUNITY_NOT_FOUND", {
			name: parsedArgs.name,
			publicKey: parsedArgs.publicKey
		});
		const cached = this._exportCommunityInstances.get(address);
		if (cached) return cached;
		const started = findStartedCommunity(this.pkc, parsedArgs);
		if (started instanceof LocalCommunity) {
			this._exportCommunityInstances.set(address, started);
			return started;
		}
		const community = await this.pkc.createCommunity({ address });
		if (!(community instanceof LocalCommunity)) throw new PKCError("ERR_COMMUNITY_NOT_LOCAL", { address });
		this._exportCommunityInstances.set(address, community);
		return community;
	}
	_toWireExportRecord(rec) {
		if (rec.progress === 1 && rec.url) return {
			...rec,
			url: `/exports/${rec.exportId}`
		};
		return rec;
	}
	async exportCommunity(params, connectionId) {
		const parsedArgs = parseRpcExportCommunityParam(params[0]);
		if (parsedArgs.includePrivateKey === true && !this._allowPrivateKeyExport) throw new PKCError("ERR_PRIVATE_KEY_EXPORT_NOT_ALLOWED", {});
		return (await this._resolveLocalCommunityForExport(parsedArgs)).export({ includePrivateKey: parsedArgs.includePrivateKey });
	}
	async exportCommunityModLogs(params) {
		const { name, publicKey, startTimestamp, endTimestamp, commentCid, limit, order } = parseRpcExportCommunityModLogsParam(params[0]);
		const address = this._findCommunityAddress({
			name,
			publicKey
		});
		if (!address) throw new PKCError("ERR_COMMUNITY_NOT_FOUND", {
			name,
			publicKey
		});
		let community;
		if (this._startedCommunities[address] instanceof LocalCommunity) community = this._startedCommunities[address];
		else {
			const created = await (await this._getPKCInstance()).createCommunity({ address });
			if (!(created instanceof LocalCommunity)) throw new PKCError("ERR_COMMUNITY_NOT_LOCAL", { address });
			community = created;
		}
		return community.exportCommunityModLogs({
			startTimestamp,
			endTimestamp,
			commentCid,
			limit,
			order
		});
	}
	async exportsSubscribe(params, connectionId) {
		const parsedArgs = parseRpcCommunityIdentifierParam(params[0]);
		const subscriptionId = generateSubscriptionId();
		const community = await this._resolveLocalCommunityForExport(parsedArgs);
		const sendEvent = (event, result) => this.jsonRpcSendNotification({
			method: "exportschange",
			subscription: subscriptionId,
			event,
			result,
			connectionId
		});
		const exportschangeListener = (records) => sendEvent("exportschange", { records: records.map((r) => this._toWireExportRecord(r)) });
		community.on("exportschange", exportschangeListener);
		this.subscriptionCleanups[connectionId][subscriptionId] = async () => {
			community.removeListener("exportschange", exportschangeListener);
		};
		sendEvent("exportschange", { records: community.exports.map((r) => this._toWireExportRecord(r)) });
		return { subscriptionId };
	}
	async cancelExport(params, connectionId) {
		const { exportId } = parseRpcCancelExportParam(params[0]);
		for (const community of this._exportLoadedCommunities()) if (community._activeExports.has(exportId)) {
			await community._cancelExport(exportId);
			break;
		}
		return { success: true };
	}
	*_exportLoadedCommunities() {
		for (const value of Object.values(this._startedCommunities)) if (value instanceof LocalCommunity) yield value;
		for (const value of this._exportCommunityInstances.values()) yield value;
	}
	_findCommunityOwningExport(exportId) {
		for (const community of this._exportLoadedCommunities()) {
			const record = community._exports.find((r) => r.exportId === exportId);
			if (record) return {
				community,
				record
			};
		}
	}
	async _sweepOldExportFiles() {
		const MAX_AGE_MS = this._exportFileMaxAgeMs;
		if (MAX_AGE_MS === 0) return;
		if (!this.pkc.dataPath) return;
		const exportsDir = path.join(this.pkc.dataPath, "exports");
		if (!existsSync(exportsDir)) return;
		const now = Date.now();
		let entries = [];
		try {
			entries = await promises.readdir(exportsDir);
		} catch {
			return;
		}
		for (const entry of entries) {
			if (!entry.endsWith(".sqlite")) continue;
			const filePath = path.join(exportsDir, entry);
			try {
				if (now - (await promises.stat(filePath)).mtimeMs > MAX_AGE_MS) await promises.unlink(filePath);
			} catch {}
		}
	}
	async _handleExportsHttpRequest(req, res) {
		const httpLog = logger_default("pkc-js:PKCWsServer:exports-http");
		const match = new URL(req.url ?? "/", "http://localhost").pathname.match(/^\/exports\/([0-9a-fA-F-]{36})$/);
		if (!match) {
			if (this._ownsHttpServer) {
				res.statusCode = 404;
				res.end();
			}
			return;
		}
		if (req.method !== "GET") {
			res.statusCode = 405;
			res.setHeader("Allow", "GET");
			res.end();
			return;
		}
		const exportId = match[1];
		const owner = this._findCommunityOwningExport(exportId);
		const community = owner?.community;
		let candidatePath;
		if (owner) {
			const { record } = owner;
			if (record.progress === 1 && record.url) try {
				const parsed = new URL(record.url);
				if (parsed.protocol === "file:") candidatePath = fileURLToPath(parsed);
			} catch {
				candidatePath = void 0;
			}
		} else if (this.pkc.dataPath) candidatePath = path.join(this.pkc.dataPath, "exports", `${exportId}.sqlite`);
		if (!candidatePath) {
			httpLog("GET /exports — unknown exportId", exportId);
			res.statusCode = 404;
			res.end();
			return;
		}
		const filePath = candidatePath;
		let size;
		try {
			size = statSync(filePath).size;
		} catch {
			res.statusCode = 404;
			res.end();
			return;
		}
		res.statusCode = 200;
		res.setHeader("Content-Type", "application/vnd.sqlite3");
		res.setHeader("Content-Length", String(size));
		const stream = createReadStream(filePath);
		let streamEnded = false;
		let streamErrored = false;
		stream.on("end", () => {
			streamEnded = true;
		});
		stream.on("error", (err) => {
			streamErrored = true;
			httpLog.error("Error streaming export file", filePath, err);
			if (!res.headersSent) res.statusCode = 500;
			res.end();
		});
		res.on("close", () => {
			if (streamErrored || !streamEnded || !res.writableEnded) return;
			if (community) community._deleteExport(exportId).catch((e) => httpLog.error("Failed to cleanup downloaded export", exportId, e));
			else promises.unlink(filePath).catch((e) => httpLog.error("Failed to cleanup downloaded export file", exportId, e));
		});
		stream.pipe(res);
	}
	async unsubscribe(params, connectionId) {
		const { subscriptionId } = parseRpcUnsubscribeParam(params[0]);
		log("Received unsubscribe", {
			connectionId,
			subscriptionId
		});
		const connectionCleanups = this.subscriptionCleanups[connectionId];
		if (!connectionCleanups || !connectionCleanups[subscriptionId]) return { success: true };
		await connectionCleanups[subscriptionId]();
		delete connectionCleanups[subscriptionId];
		return { success: true };
	}
	async destroy() {
		for (const connectionId of import_commonjs.keys.strict(this.subscriptionCleanups)) for (const subscriptionId of import_commonjs.keys.strict(this.subscriptionCleanups[connectionId])) await this.unsubscribe([{ subscriptionId: Number(subscriptionId) }], connectionId);
		this.ws.close();
		if (this._ownsHttpServer && this._httpServer) await new Promise((r) => this._httpServer.close(() => r()));
		await (await this._getPKCInstance()).destroy();
		for (const communityAddress of import_commonjs.keys.strict(this._startedCommunities)) delete this._startedCommunities[communityAddress];
		this._exportCommunityInstances.clear();
		this._rpcStateDb?.close();
		this._rpcStateDb = void 0;
		this._onSettingsChange = {};
	}
};
const createPKCWsServer = async (options) => {
	const parsedOptions = parseCreatePKCWsServerOptionsSchemaWithPKCErrorIfItFails(options);
	const pkcWss = new PKCWsServer({
		pkc: await PKCJs.PKC(parsedOptions.pkcOptions),
		port: parsedOptions.port,
		server: parsedOptions.server,
		authKey: parsedOptions.authKey,
		startStartedCommunitiesOnStartup: parsedOptions.startStartedCommunitiesOnStartup,
		autoStartConcurrency: parsedOptions.autoStartConcurrency,
		allowPrivateKeyExport: parsedOptions.allowPrivateKeyExport,
		exportFileMaxAgeMs: parsedOptions.exportFileMaxAgeMs
	});
	pkcWss._autoStartPreviousCommunities().catch((e) => {
		log.error("Failed to auto-start previous communities", e);
	});
	pkcWss._sweepOldExportFiles().catch((e) => {
		log.error("Failed to sweep old export files", e);
	});
	return pkcWss;
};
const PKCRpc = {
	PKCWsServer: createPKCWsServer,
	setPKCJs
};
//#endregion
export { PKCRpc as default };
