Skip to content

feat(community): add bitsocial community export command - #65

Merged
Rinse12 merged 5 commits into
masterfrom
feat/community-export
Jun 7, 2026
Merged

feat(community): add bitsocial community export command#65
Rinse12 merged 5 commits into
masterfrom
feat/community-export

Conversation

@Rinse12

@Rinse12 Rinse12 commented Jun 6, 2026

Copy link
Copy Markdown
Member

Closes #64

Summary

Upgrades @pkcprotocol/pkc-js 0.0.40 → 0.0.41 and exposes its new community export API (pkcprotocol/pkc-js#100) in the CLI.

bitsocial community export [ADDRESS]

Triggers an export on the RPC server (daemon), streams progress to stderr, downloads the finished SQLite snapshot via GET /exports/<exportId>, and verifies its sha256 against the export record before reporting success.

Flag Behavior
ADDRESS / --name / --publicKey community lookup, same trio as community get
-o, --path local destination file (default ./<address>.sqlite)
--includePrivateKey ask the server to include the community signer's private key (off by default; required for a restorable backup that keeps the same address)
--force overwrite an existing destination file
--quiet suppress stderr progress; stdout gets only the final path

Implementation details:

  • Downloads to <dest>.partial and atomically renames after the sha256 check, so a corrupted/interrupted download never clobbers the destination.
  • Ctrl+C aborts via AbortSignalcancelExport() on the server; a second Ctrl+C force-exits.
  • Errors cleanly when the community is not local to the RPC server (remote communities can't be exported).

bitsocial daemon --allowPrivateKeyExport

Negatable boolean (default true, matching the pkc-js default), plumbed through startDaemonServerPKCWsServer. With --no-allowPrivateKeyExport, RPC clients requesting --includePrivateKey get ERR_PRIVATE_KEY_EXPORT_NOT_ALLOWED.

Tests

11 new tests in test/cli/export.community.test.ts:

  • happy path with a real HTTP server download + sha256 verification + stdout path
  • lookup variants (--name, --publicKey), missing-identifier error
  • --includePrivateKey pass-through to community.export()
  • overwrite protection without --force (existing file untouched, export never starts) and overwrite with --force
  • sha256 mismatch fails and leaves neither the destination nor a .partial behind
  • export error record (ERR_PRIVATE_KEY_EXPORT_NOT_ALLOWED) fails the command
  • non-local community error
  • daemon flag definition (negatable, default true)

Full npm run test:cli suite: 220 passed, 1 skipped (pre-existing), 29 files.

Summary by CodeRabbit

  • New Features

    • Export community snapshots locally with SHA-256 verification, real-time progress output, Ctrl+C cancellation, and force-overwrite; lookup by address, name, or public key
    • Daemon flag to control private-key export permissions (enabled by default; disable with --no-allowPrivateKeyExport)
  • Tests

    • Comprehensive test coverage for the export workflow, edge cases, verification, cancellation, and daemon flag behavior
  • Chores

    • Bumped @pkcprotocol/pkc-js to 0.0.41

Rinse12 added 2 commits June 6, 2026 12:29
Exposes the pkc-js 0.0.41 community export API (pkcprotocol/pkc-js#100):

- `bitsocial community export [ADDRESS]` triggers an export on the RPC
  server, streams progress to stderr, downloads the finished SQLite
  snapshot via GET /exports/<exportId>, verifies its sha256 against the
  export record (.partial file + atomic rename so a corrupted download
  never clobbers the destination), and prints the destination path.
  Flags: --name/--publicKey lookup, -o/--path, --includePrivateKey,
  --force, --quiet. Ctrl+C cancels the in-flight export server-side.
- `bitsocial daemon --allowPrivateKeyExport` (negatable, default true,
  matching pkc-js) controls whether RPC clients may request exports
  that include the community signer's private key.
@coderabbitai

coderabbitai Bot commented Jun 6, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@Rinse12, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 13 minutes and 21 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f860a75f-e5bc-4f9c-9ce4-05b7aeae7345

📥 Commits

Reviewing files that changed from the base of the PR and between 779a85f and 0e40b4f.

📒 Files selected for processing (4)
  • src/cli/commands/community/export.ts
  • src/webui/daemon-server.ts
  • test/cli/export.community.test.ts
  • test/webui/daemon-server.test.ts
📝 Walkthrough

Walkthrough

Adds bitsocial community export CLI command with RPC export orchestration, snapshot download and SHA-256 verification, a daemon --allowPrivateKeyExport flag, pkc-js dependency bump, and comprehensive tests covering success and failure modes.

Changes

Community Export Feature

Layer / File(s) Summary
Pkc-js 0.0.41 Upgrade
package.json
Dependency bump from 0.0.40 to 0.0.41.
Daemon Export Policy Flag
src/cli/commands/daemon.ts, src/webui/daemon-server.ts
Adds --allowPrivateKeyExport boolean flag (negatable) and forwards it into startDaemonServerPKCWsServer.
Export Command Implementation
src/cli/commands/community/export.ts
New bitsocial community export command: validates lookup params, connects to PKC RPC, starts server export and listens for exportschange progress, supports Ctrl+C abort, downloads snapshot to .partial while streaming into SHA-256, enforces origin/path, verifies checksum, and atomically renames on success.
Export Command and Daemon Flag Tests
test/cli/export.community.test.ts
Vitest suite mocking RPC and HTTP server: happy path, lookup by address/name/publicKey, --includePrivateKey, overwrite/--force, sha256 mismatch, export-record errors, origin enforcement, cancellation, non-local community handling, and daemon flag assertion.

Sequence Diagram(s)

sequenceDiagram
  participant CLI as Export Command
  participant PKC as PKC RPC (WS)
  participant HTTP as Snapshot HTTP Server
  participant FS as Local File System
  CLI->>PKC: connect(rpcUrl)
  CLI->>PKC: community.export({includePrivateKey}, signal)
  PKC-->>CLI: emits exportschange events (progress / error / url / sha256)
  CLI->>PKC: wait until progress === 1 (or abort / error)
  CLI->>HTTP: fetch(record.url, {signal})
  HTTP-->>CLI: stream bytes
  CLI->>FS: write to .partial + update SHA-256
  CLI->>CLI: verify sha256 matches record.sha256 (if present)
  CLI->>FS: rename .partial → destination
  CLI->>PKC: destroy() on cleanup
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 I hopped along the RPC stream,

Fetched a snapshot, hashed its gleam,
Ctrl+C to stop the race,
A flag keeps private keys in place,
Saved the file — a bunny's dream.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(community): add bitsocial community export command' accurately and concisely describes the main change—adding a new community export CLI command.
Linked Issues check ✅ Passed The PR fully implements all coding requirements from issue #64: pkc-js upgrade, community export command with all specified flags and behaviors, daemon allowPrivateKeyExport flag, and comprehensive test coverage.
Out of Scope Changes check ✅ Passed All changes directly support issue #64 objectives: version bump, new CLI command, daemon flag, function signature update, and tests—no extraneous modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/community-export

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
test/cli/export.community.test.ts (1)

117-230: 🏗️ Heavy lift

Add a cancellation-path test for SIGINT-driven abort.

The new command behavior explicitly supports Ctrl+C cancellation, but this suite doesn’t assert that an in-flight export is aborted and exits cleanly when interrupted.

As per coding guidelines, Add a test when you add a feature or fix a bug.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/cli/export.community.test.ts` around lines 117 - 230, Add a new test
that simulates a SIGINT during an in-flight export: use runCliCommand to start
`community export` (same pattern as other tests, e.g. destination path like
"sigint.sqlite"), ensure the fake export server (exportFake / servedContent) is
in a state that will block/stream so the command is mid-download, send a SIGINT
to the child process (as runCliCommand exposes the spawned process or via the
test helper), then assert the CLI exits with an error/aborted result, stderr
contains an abort message (or "SIGINT"), no final destination file exists and no
".partial" file remains, and clean up by verifying destroyFake was called;
follow the style of existing tests (expect(result.error).toBeDefined(),
fs.stat(...) rejects, exportFake/destroyFake assertions).

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/cli/commands/community/export.ts`:
- Around line 185-189: The code currently calls fetch(record.url!) inside
_downloadAndVerify which trusts server-provided URLs; change it to construct the
download request against the trusted daemon origin instead of using record.url
directly. Locate the _downloadAndVerify method and replace the direct
fetch(record.url!) with building a URL using the daemon/RPC base origin (e.g.,
daemonBaseUrl or the configured RPC origin) plus the path/filename from
record.url (extract pathname/search/hash from record.url) and then fetch against
that constructed URL; ensure you validate or normalize the resulting URL and
preserve the same AbortSignal and headers when calling fetch.
- Around line 158-181: The promise returned by _runExport currently only
resolves/rejects via the exportschange listener (checkRecords) so if an external
abort/Ctrl+C occurs the promise can hang; modify _runExport to also listen to
the abort signal (or accept an AbortSignal) and on abort remove the
exportschange listener (community.removeListener("exportschange", checkRecords))
then reject the promise with an AbortError (or new Error('aborted')) so the
caller doesn't wait indefinitely; ensure exportId, checkRecords and the existing
this._printProgress logic remain unchanged while adding the abort cleanup and
rejection path.

---

Nitpick comments:
In `@test/cli/export.community.test.ts`:
- Around line 117-230: Add a new test that simulates a SIGINT during an
in-flight export: use runCliCommand to start `community export` (same pattern as
other tests, e.g. destination path like "sigint.sqlite"), ensure the fake export
server (exportFake / servedContent) is in a state that will block/stream so the
command is mid-download, send a SIGINT to the child process (as runCliCommand
exposes the spawned process or via the test helper), then assert the CLI exits
with an error/aborted result, stderr contains an abort message (or "SIGINT"), no
final destination file exists and no ".partial" file remains, and clean up by
verifying destroyFake was called; follow the style of existing tests
(expect(result.error).toBeDefined(), fs.stat(...) rejects,
exportFake/destroyFake assertions).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: dbfb0072-5766-4b33-bed8-22d418c3ee40

📥 Commits

Reviewing files that changed from the base of the PR and between b662d4c and 53821e4.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (5)
  • package.json
  • src/cli/commands/community/export.ts
  • src/cli/commands/daemon.ts
  • src/webui/daemon-server.ts
  • test/cli/export.community.test.ts

Comment thread src/cli/commands/community/export.ts
Comment thread src/cli/commands/community/export.ts Outdated
- Reject _runExport() on abort instead of waiting for the server's
  terminal ERR_EXPORT_CANCELLED record, which never arrives if the
  daemon died or the connection dropped. pkc-js's own abort listener
  (registered first inside community.export()) still dispatches
  cancelExport() to the server.
- Validate the export record's download URL against the RPC server's
  http origin (ws[s]:// swapped to http[s]://, mirroring pkc-js's
  rpcHttpOrigin) and require a /exports/ path, so a misconfigured or
  compromised RPC server can't use the CLI to fetch arbitrary URLs.
- Add tests for both: wrong-origin URL is refused with no request
  made, and an aborted export with a silent daemon rejects instead
  of hanging.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/cli/commands/community/export.ts (1)

232-243: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fail closed when the terminal export record omits sha256.

record.sha256 is optional here, so a terminal record without a checksum still gets written to disk and the CLI prints Verified sha256 (undefined). That silently drops the integrity guarantee this command advertises. Reject the export when the terminal record has no hash, and add a regression test for that path.

Suggested fix
             const downloadedSha256 = hash.digest("hex");
-            if (record.sha256 && downloadedSha256 !== record.sha256) {
+            if (!record.sha256) {
+                throw new Error(`Export ${record.exportId} finished without a sha256 checksum`);
+            }
+            if (downloadedSha256 !== record.sha256) {
                 throw new Error(
                     `sha256 mismatch for downloaded export: expected ${record.sha256} but downloaded file hashes to ${downloadedSha256}`
                 );
             }
             await fs.rename(partialPath, destPath);

As per coding guidelines, Add a test when you add a feature or fix a bug.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli/commands/community/export.ts` around lines 232 - 243, The code
currently allows writing a terminal export record with no checksum
(record.sha256) and prints "Verified sha256 (undefined)"; change the logic in
the download/verify block (the code that computes downloadedSha256, compares to
record.sha256, renames partialPath to destPath, and calls this._printProgress)
to explicitly reject and throw an error when record.sha256 is missing before
attempting verification or file move, ensure partialPath is removed on error as
already done, and add a regression test that simulates an export record with
record.sha256 === undefined to assert the CLI fails rather than writing the
file.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/cli/commands/community/export.ts`:
- Around line 232-243: The code currently allows writing a terminal export
record with no checksum (record.sha256) and prints "Verified sha256
(undefined)"; change the logic in the download/verify block (the code that
computes downloadedSha256, compares to record.sha256, renames partialPath to
destPath, and calls this._printProgress) to explicitly reject and throw an error
when record.sha256 is missing before attempting verification or file move,
ensure partialPath is removed on error as already done, and add a regression
test that simulates an export record with record.sha256 === undefined to assert
the CLI fails rather than writing the file.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5a5f0220-5c64-4134-b2ec-6015e87c1444

📥 Commits

Reviewing files that changed from the base of the PR and between 53821e4 and 779a85f.

📒 Files selected for processing (2)
  • src/cli/commands/community/export.ts
  • test/cli/export.community.test.ts

Rinse12 added 2 commits June 7, 2026 05:05
…dress>_<datetime>.sqlite

The default -o/--path was ./<address>.sqlite (cwd-relative). Snapshots now
land in the exports directory inside the data path, with an ISO 8601
datetime (':' -> '-', matching the daemon log filename convention) so
repeated exports never collide and sort chronologically. -o still
overrides entirely.

Also closes the export command's test coverage gaps found in review:
- HTTP non-200 download response and mid-stream connection drop
  (no destination file or .partial left behind)
- finished record with no download URL
- same-origin-but-non-/exports download URL refused (second half of the
  URL validation), wss:// RPC URL -> https:// expected download origin
- record without sha256 skips verification (pins current behavior)
- default destination directory + filename pattern (XDG_DATA_HOME
  pointed at a temp dir so tests never write into a real data path)
- intermediate destination directories created; destination-is-a-directory
  with --force fails cleanly
- address + --name combined into a single lookup
- non-quiet mode: progress on stderr, stdout only the destination path;
  --quiet asserted to suppress progress
- pkc.destroy() asserted on all failure paths (connection-leak coverage)
- _runExport unit tests: terminal success/error record already present
  before listener attach, pre-aborted signal, records of other exportIds
  ignored
…wnloads

Live `community export` against a real daemon always failed with HTTP 404
even though the export completed server-side. The daemon serves everything
on one port: startDaemonServer creates the http.Server via express().listen()
and hands it to PKCWsServer, which attaches a second request listener to
stream GET /exports/<exportId>. Both listeners get every request; express
finds no matching route and 404s synchronously while pkc-js's handler is
still doing async fs work, so express wins the race.

Fix: an express middleware that swallows /exports/<uuid> paths (responds
nothing, calls nothing) so pkc-js's listener owns the response. All other
/exports/* paths fall through to express's 404 because pkc-js ignores them
on a caller-supplied server and the request would otherwise hang.

The middleware is deliberately NOT mounted at app.use("/exports", ...):
a mounted middleware strips the mount prefix from the shared req.url while
the request is held, so pkc-js's listener would no longer recognize the
URL and the download would hang instead.

Regression test mimics pkc-js's listener answering after a short delay
(a synchronous fake wins the race and hides the bug); verified it fails
with 404 against the unfixed build. Also adds plumbing tests asserting
daemon --allowPrivateKeyExport reaches PKCWsServer verbatim
(true/false/omitted).
@Rinse12

Rinse12 commented Jun 7, 2026

Copy link
Copy Markdown
Member Author

Two new commits since the CodeRabbit review:

52c5ac1 feat(community): default export destination to <dataPath>/exports/<address>_<datetime>.sqlite

  • Default -o was ./<address>.sqlite (cwd-relative); snapshots now land in <dataPath>/exports/ with a datetime suffix (ISO 8601, :-, same convention as daemon log filenames) so repeated exports never collide. -o still overrides.
  • Also closes the test coverage gaps found in review — export tests went from 11 to 28 (HTTP errors, mid-stream drops, URL validation halves, wss→https origin mapping, missing sha256/url, default path, destroy-on-failure, _runExport race/abort unit tests, non-quiet stdout/stderr contract).

0e40b4f fix(daemon): stop express's catch-all 404 from clobbering /exports downloads

  • Found by running a live export against a real daemon: the download always 404'd because express and pkc-js's /exports/<id> handler share one http.Server, and express's synchronous 404 beat the async pkc-js handler. Fixed with a middleware that stays silent for /exports/<uuid> (deliberately unmounted — app.use("/exports", …) mutates the shared req.url and turns the 404 into a hang).
  • Regression test verified to fail (404) against the unfixed build; plumbing tests assert --allowPrivateKeyExport reaches PKCWsServer verbatim.

Verified live end-to-end (export → download → sha256 verify → <dataPath>/exports/…_2026-06-07T05-03-35.773Z.sqlite, valid SQLite db, exit 0). Full suite: 241 passed, 1 skipped, 29 files.

Analysis details in #64.

@Rinse12
Rinse12 merged commit f0dc03f into master Jun 7, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add bitsocial community export command (pkc-js 0.0.41 export API)

1 participant