Conversation
The devnet ckb.toml template enables the Terminal RPC module, which only exists since CKB v0.205.0 (nervosnetwork/ckb#4989). Older binaries abort at startup with an opaque serde "unknown variant `Terminal`" error, and migrateLegacyDevnetRpcConfig re-added the module on every `offckb node` start even after users removed it by hand — an unbreakable crash loop for anyone pinned to an old CKB. - initChainIfNeeded takes the effective CKB version: fresh chains for a pre-0.205.0 binary are initialized from the template with Terminal stripped (tcp_listen_address, which predates 0.205.0, stays), and the legacy-config migration no longer re-adds Terminal for such binaries. - nodeDevnet resolves the effective version (managed binaries know it; a custom --binary-path is probed via getVersionFromBinary) and fails fast with an actionable error when the existing config enables Terminal but the binary is too old, instead of letting CKB dump the serde error. - A custom binary whose version cannot be probed keeps the historical behavior; if it then crashes with the tell-tale "unknown variant `Terminal`" stderr, the startup error now points at the actual cause. - README's status section notes the CKB >= 0.205.0 requirement for the TUI's system-metric panels. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(status): bump ckb-tui to v0.1.4 and reinstall stale binaries ckb-tui v0.1.4 fixes a divide-by-zero panic in the dashboard data-sync thread when the connected node has no peers (Officeyutong/ckb-tui#13) — the normal state of a single-node devnet — which permanently froze the Overview, Mempool, Peers, and Blockchain panels seconds after opening offckb status. - Bump the default ckb-tui version to v0.1.4 and pin the SHA-256 digests of its release assets. - Pin digests of the extracted binaries as well and verify the installed binary against them in ensureInstalled(): ckb-tui's --version output lags its release tag (both v0.1.3 and v0.1.4 binaries report 0.1.2), so a presence-only check would keep the panic-affected v0.1.3 binary installed forever. A stale or foreign binary is now removed and reinstalled; versions without a pinned binary digest keep the previous presence-only behavior. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(status): keep the previous ckb-tui binary until the reinstall succeeds Address review feedback on #476: - ensureInstalled no longer deletes the installed binary up front. installSync already stages the download/verify/extract in a temp directory and publishes with an atomic rename, so a failed reinstall now leaves the previous binary untouched instead of stranding the user with no executable. - installedBinaryMatches treats missing, unreadable, or non-regular paths (e.g. a directory at the binary location) as a mismatch and flows into the reinstall path instead of throwing raw fs errors out of ensureInstalled. - Tests: the install spy now publishes the binary path like a real installSync, so the ensureInstalled return contract is asserted; added regression tests for failed-reinstall preservation and for non-regular paths. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(status): set aside directory targets and strict-check the ckb-tui binary Second round of CodeRabbit review on the ckb-tui v0.1.4 bump: - A directory occupying the install path made every reinstall fail with EISDIR (a file rename cannot replace a directory). publishExtractedBinary now sets the directory aside with a plain rename — its contents are never deleted — publishes the verified binary, and restores the directory if publishing fails. - installedBinaryMatches required a regular, readable file only on some paths: the pinned branch hashed whatever was there, so a FIFO at the install path blocked ensureInstalled indefinitely, and the unpinned fallback accepted unreadable regular files. It now checks isFile() and R_OK up front for both branches. - The successful install test spy now publishes a real regular file (and assertions verify type/content), with new regression tests for directory publish, restore-on-failure, FIFO, and unreadable binaries. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(status): don't let post-publish cleanup mask a successful ckb-tui install Follow-up to the re-review of 0e1dd83: - renameIntoPlace no longer unlinks the staged source after the publish rename succeeds: a failure there would have reported a correctly published binary as a failed install. The source lives in the temp directory, which installSync's finally block removes regardless. - The FIFO regression test now mocks readFileSync to throw, so a regression fails fast (and asserts the FIFO is never opened) instead of hanging the Jest worker on a synchronous blocking read. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…de (#478) * feat: unify devnet logging with offckb logs and a quiet foreground node Add offckb logs [node|script|miner|rpc] [-f] [--grep] [--tail], reading the log files CKB always writes (run.log/miner.log) plus a new proxy event log, so logs are reachable in every run mode and pipe/agent friendly. A foreground offckb node is quiet by default: lifecycle events, live contract script debug output (via the node's TCP log subscription, the same channel ckb-tui uses), send_transaction hashes, and RPC errors still print; --verbose restores the raw stdout relay. The RPC proxy drops per-request lines to debug, warns on JSON-RPC errors in responses, and appends everything to data/logs/proxy.log (viewable via offckb logs rpc). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: address PR review findings on logs command and proxy events - node: sanitize relayed script log entries (CSI/OSC/C0/C1) via cleanChildOutput so crafted debug! output cannot inject terminal control sequences - log-file: honor readSync's byte count and decode with a streaming TextDecoder so multi-byte UTF-8 survives chunk boundaries - proxy-events: sanitize event text at the single event() choke point (one event = one line), normalize the response media type before the application/json check (charset params), and bound proxy.log with a single .1 rollover at 10 MB - log-subscription: retry only during the initial connect window and track/unref/clear the retry timer so close() is fully synchronous - cli: throw commander's InvalidArgumentError from the --tail parser and align the --verbose help text with the actual quiet defaults - tests: add follow-mode script/grep, truncation/rotation, UTF-8 split, event sanitization, rollover, charset content-type, and subscription retry cases; move temp-dir handling to afterEach cleanup --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
fix(config): unfreeze bundled ckb-tui version for upgraded installs
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* test: add legacy-data upgrade-path integration test to CI Simulate a user upgrading offckb: an old release (0.3.4, CKB 0.113.1) creates a devnet in a sandboxed HOME/XDG, then the current build must operate on that legacy data without breaking it: - the chain continues (same genesis hash, tip grows past the old tip), so a silent chain reset cannot pass as 'RPC responds' - the legacy ckb.toml is migrated (Terminal RPC module + tcp_listen_address) — the class of bug that previously reached users before we noticed - the bundled chain spec stays byte-identical - a fresh transfer succeeds and is committed Runs per-PR on ubuntu-latest only, after create-test; the npm cache is cached to keep the extra legacy CLI install cheap. Verified end-to-end locally twice (offckb 0.3.4 → 0.4.10). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci: run legacy-data test before create-test create-test.sh's cleanup kills only the pnpm wrapper, leaving the CKB processes orphaned and holding ports 8114/28114 (the runner's orphan cleanup only fires at job end), which tripped the legacy test's precondition check. Run the legacy test first instead — its own teardown is complete, so create-test still starts on free ports. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
chore: version packages for 0.4.11 release
|
❌ Missing Changeset Please add a changeset describing your changes: pnpm changesetIf your changes do not need a version bump (docs, CI, refactoring), For dependency updates, use the |
📝 WalkthroughSummary by CodeRabbit
WalkthroughThe CLI release adds unified devnet logging, quiet and verbose node output, version-aware Terminal RPC configuration, legacy-data upgrade testing, ckb-tui v0.1.4 verification, persisted settings migration, and the 0.4.11 release documentation. ChangesUnified devnet logging
CKB compatibility and legacy upgrades
Settings and ckb-tui release
Estimated code review effort: 5 (Critical) | ~90 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 8
🧹 Nitpick comments (7)
tests/ckb-tui-install.test.ts (2)
199-237: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd coverage for the EXDEV staging branch.
The three tests cover the direct-rename path and the directory backup and restore paths. No test reaches the
EXDEVfallback inrenameIntoPlace, which stages a copy next to the target and then renames it. That branch also runsfs.rmSync(stagingPath, { force: true })in afinallyblock after a successful rename. Force the branch by making the firstfs.renameSynccall throw anEXDEVerror.♻️ Proposed additional test
it('restores the original directory when publishing fails', () => {it('stages the copy next to the target when the rename crosses filesystems', () => { const extracted = path.join(binDir, 'extracted-ckb-tui'); fs.writeFileSync(extracted, REPLACED_BINARY); const real = fs.renameSync; let firstCall = true; jest.spyOn(fs, 'renameSync').mockImplementation((from, to) => { if (firstCall) { firstCall = false; const error = new Error('EXDEV: cross-device link not permitted') as NodeJS.ErrnoException; error.code = 'EXDEV'; throw error; } return real(from, to); }); internals.publishExtractedBinary(extracted, binaryPath); expect(fs.readFileSync(binaryPath, 'utf8')).toBe(REPLACED_BINARY); // No staging file is left behind. expect(fs.readdirSync(binDir).filter((name) => name.includes('.staging-'))).toHaveLength(0); jest.restoreAllMocks(); });🤖 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 `@tests/ckb-tui-install.test.ts` around lines 199 - 237, Add a test covering the EXDEV fallback in renameIntoPlace by mocking the first fs.renameSync call to throw an error with code EXDEV, then delegating subsequent calls to the real implementation. Use internals.publishExtractedBinary with a valid extracted binary, assert the target contains the replacement content, and verify no .staging- file remains; ensure the fs.renameSync mock is restored.
88-107: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider importing the pinned digest table instead of duplicating it.
pinnedDigest()copies the three v0.1.4 entries fromKNOWN_BINARY_SHA256insrc/tools/ckb-tui.ts. The comment states that the copy is kept in sync by hand. Export the table from the module and read it here, so a digest change cannot leave the test asserting a stale value.The mocked
crypto.createHashat lines 101-103 returns the pinned digest for any input. The test therefore verifies the comparison and the keep decision, but not thatinstalledBinaryMatcheshashes the file bytes. Consider computing the digest of a fixture file and injecting that value into the mocked settings-driven lookup instead, so the test exercises the real hash path.🤖 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 `@tests/ckb-tui-install.test.ts` around lines 88 - 107, Update the test’s pinnedDigest setup to import and read the exported KNOWN_BINARY_SHA256 table from ckb-tui.ts instead of duplicating its entries. Also revise the existing-binary test around CKBTui.ensureInstalled and crypto.createHash so the mock returns the digest computed from the fixture file bytes through the settings-driven lookup, preserving verification of the real installedBinaryMatches hashing path while still asserting installation is skipped.tests/setting.test.ts (1)
79-95: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider asserting that sibling
toolskeys survive the delete.
writeSettingsdeletestoWrite.tools.ckbTui. No test asserts that othertoolsentries, such asrootFolder, remain in the written file. A future change fromdelete tools.ckbTuito replacingtoolswould pass the current suite.♻️ Proposed additional assertion
it('omits the bundled ckb-tui version when it equals the shipped default', () => { const settings = readSettings(); writeSettings(settings); const written = JSON.parse(fs.readFileSync(configPath, 'utf8')); expect(written.tools.ckbTui).toBeUndefined(); + // Sibling tools entries must survive the delete. + expect(written.tools.rootFolder).toBe(defaultSettings.tools.rootFolder); });🤖 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 `@tests/setting.test.ts` around lines 79 - 95, Add an assertion in the writeSettings tests that the sibling tools.rootFolder value remains present and unchanged after writeSettings deletes or omits tools.ckbTui, covering both the default-version and differing-version cases as appropriate.tests/init-chain.test.ts (1)
210-227: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd coverage for
devnetConfigHasTerminalRpc.This suite covers
supportsTerminalRpcModuleand the migration, but no test exercisesdevnetConfigHasTerminalRpcwith real files. That function gates the fail-fast path at src/cmd/node.ts:105, and tests/node-terminal-rpc.test.ts mocks it. Add cases for a Terminal-enabled config, a config without Terminal, a missing file, and an unparseable file.Do you want me to generate those tests?
🤖 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 `@tests/init-chain.test.ts` around lines 210 - 227, Add tests in the “Terminal RPC module version gating” suite that exercise devnetConfigHasTerminalRpc using real temporary config files: verify Terminal-enabled, Terminal-absent, missing-file, and unparseable-file cases. Reuse the existing root and mockConfigPath setup, and assert the function’s expected boolean result for each scenario.src/cmd/logs.ts (1)
38-51: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueReturn the stop function from
showLogs.
showLogsdiscards the value returned byfollowLogFile. The process stays alive becausefs.watchFileholds the event loop, and Ctrl-C terminates it. But no caller and no test can stop the follow. Return the stop function so callers control the subscription.♻️ Proposed refactor
-export function showLogs(target: LogTarget, options: LogsOptions, settings: Settings, logger: UnifiedLogger): void { +export function showLogs( + target: LogTarget, + options: LogsOptions, + settings: Settings, + logger: UnifiedLogger, +): (() => void) | undefined { @@ - if (!options.follow) return; + if (!options.follow) return undefined; let inScriptEntry = false; - followLogFile(filePath, (line) => { + return followLogFile(filePath, (line) => {🤖 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/cmd/logs.ts` around lines 38 - 51, Update showLogs to return the stop function produced by followLogFile instead of discarding it, while preserving the existing early return when following is disabled and the log filtering behavior.src/devnet/log-file.ts (2)
88-91: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider a network parameter for the
rpctarget.
resolveLogPathhardcodesNetwork.devinet… correction: it hardcodesNetwork.devnetforrpc.createRPCProxyinsrc/tools/rpc-proxy.tswrites proxy events for testnet and mainnet too, usingproxyLogPathForNetwork(network, settings). Those files are then unreachable fromoffckb logs rpc. Add an optional network argument when the CLI gains a--networkflag forlogs.🤖 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/devnet/log-file.ts` around lines 88 - 91, The resolveLogPath function hardcodes Network.devnet for rpc logs, preventing testnet and mainnet proxy logs from being resolved. Add an optional network parameter to resolveLogPath and use it with proxyLogPathForNetwork for the rpc target, then propagate the CLI’s --network value through the logs command while preserving devnet as the default.
93-108: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winRead the tail without loading the whole log file.
readLogTailreads the complete file into a string.run.loggrows without bound during a long devnet session, so aoffckb logscall allocates the entire file to print 100 lines. For very large files Node also throws a string-length error instead of the helpful message.Read backwards in fixed-size chunks until
countnewlines are found.♻️ Proposed bounded tail read
export function readLogTail(filePath: string, count: number): string[] { - let content: string; + const CHUNK = 64 * 1024; + let fd: number; try { - content = fs.readFileSync(filePath, 'utf8'); + fd = fs.openSync(filePath, 'r'); } catch (error) { const err = error as NodeJS.ErrnoException; if (err.code === 'ENOENT') { throw new Error( `Log file not found at ${filePath}. Start the devnet first (offckb node) and make sure ` + 'ckb.toml keeps log_to_file = true.', ); } throw error; } - return tailLines(content, count); + try { + let position = fs.fstatSync(fd).size; + const chunks: Buffer[] = []; + let newlines = 0; + while (position > 0 && newlines <= count) { + const length = Math.min(CHUNK, position); + position -= length; + const buffer = Buffer.alloc(length); + fs.readSync(fd, buffer, 0, length, position); + chunks.unshift(buffer); + for (const byte of buffer) if (byte === 0x0a) newlines++; + } + return tailLines(Buffer.concat(chunks).toString('utf8'), count); + } finally { + fs.closeSync(fd); + } }🤖 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/devnet/log-file.ts` around lines 93 - 108, Update readLogTail to avoid fs.readFileSync and read the log backwards in fixed-size chunks, accumulating only enough data to identify the final count lines; preserve the existing ENOENT message and tailLines behavior, including sensible handling when the requested count exceeds available lines.
🤖 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/cmd/logs.ts`:
- Around line 54-56: Update logsCommand to reject unrecognized target values
instead of defaulting them to 'node'. Validate the provided target against
LOG_TARGETS, report an error that includes the accepted values, and only call
showLogs for valid targets or when the target is omitted and should use the
default.
- Around line 32-36: Adjust the log retrieval flow around readLogTail and
filterLinesByTarget so target-filtered requests read a sufficiently large window
before filtering, then limit the filtered results to the requested tail count.
Preserve the existing grep behavior and ensure unfiltered logs continue using
the current tail semantics.
In `@src/devnet/log-file.ts`:
- Around line 124-131: Update followLogFile’s onChange reset condition in
src/devnet/log-file.ts:124-131 to also reset when curr.ino differs from
prev.ino, alongside the existing size checks. Update tests/logs.test.ts:197-201
to simulate rotation by renaming run.log, writing a larger replacement at the
original path, and asserting the replacement content is emitted from offset 0.
In `@src/node/init-chain.ts`:
- Around line 74-77: Update supportsTerminalRpcModule to use semver.satisfies
with the range >=0.205.0-0, while preserving the existing null and
invalid-version behavior. Update the related test expectation so 0.205.0
prerelease versions, including rc builds, are accepted for Terminal RPC support.
In `@src/tools/ckb-tui.ts`:
- Around line 109-138: Update installedBinaryMatches to validate execute
permission in addition to readability on POSIX systems, using fs.accessSync with
X_OK so binaries without the execute bit return false and enter the reinstall
path. Preserve the existing regular-file, digest, and fallback behavior, while
handling platforms where POSIX execute checks are not applicable.
In `@src/tools/proxy-events.ts`:
- Around line 63-76: Separate rollover operations from event appending in the
logging flow around the existing fs.renameSync and fs.appendFileSync calls. If
rollover deletion or renaming fails, retain the current log file and continue to
append the event instead of entering the catch path that drops it or leaves size
invalid; update size consistently after the append so subsequent events can
retry rollover when appropriate.
- Around line 86-111: Update handleProxyRequestBody to normalize the parsed
JSON-RPC payload into a single-item or batch array, then process each request
entry so batched send_transaction calls are recorded and persisted. Before
accessing the first parameter, validate that params is an array; skip
transaction handling for missing or non-array params instead of allowing a
TypeError to be reported as a JSON parsing failure.
In `@src/tools/rpc-proxy.ts`:
- Around line 15-23: Annotate the ctx object in the RPC proxy setup with the
ProxyEventContext type, then connect the --verbose option to the
UnifiedLogger/Winston log-level configuration so ctx.sink.debug emits proxy
request logs during verbose runs; otherwise explicitly document that --verbose
does not enable proxy debug logging.
---
Nitpick comments:
In `@src/cmd/logs.ts`:
- Around line 38-51: Update showLogs to return the stop function produced by
followLogFile instead of discarding it, while preserving the existing early
return when following is disabled and the log filtering behavior.
In `@src/devnet/log-file.ts`:
- Around line 88-91: The resolveLogPath function hardcodes Network.devnet for
rpc logs, preventing testnet and mainnet proxy logs from being resolved. Add an
optional network parameter to resolveLogPath and use it with
proxyLogPathForNetwork for the rpc target, then propagate the CLI’s --network
value through the logs command while preserving devnet as the default.
- Around line 93-108: Update readLogTail to avoid fs.readFileSync and read the
log backwards in fixed-size chunks, accumulating only enough data to identify
the final count lines; preserve the existing ENOENT message and tailLines
behavior, including sensible handling when the requested count exceeds available
lines.
In `@tests/ckb-tui-install.test.ts`:
- Around line 199-237: Add a test covering the EXDEV fallback in renameIntoPlace
by mocking the first fs.renameSync call to throw an error with code EXDEV, then
delegating subsequent calls to the real implementation. Use
internals.publishExtractedBinary with a valid extracted binary, assert the
target contains the replacement content, and verify no .staging- file remains;
ensure the fs.renameSync mock is restored.
- Around line 88-107: Update the test’s pinnedDigest setup to import and read
the exported KNOWN_BINARY_SHA256 table from ckb-tui.ts instead of duplicating
its entries. Also revise the existing-binary test around CKBTui.ensureInstalled
and crypto.createHash so the mock returns the digest computed from the fixture
file bytes through the settings-driven lookup, preserving verification of the
real installedBinaryMatches hashing path while still asserting installation is
skipped.
In `@tests/init-chain.test.ts`:
- Around line 210-227: Add tests in the “Terminal RPC module version gating”
suite that exercise devnetConfigHasTerminalRpc using real temporary config
files: verify Terminal-enabled, Terminal-absent, missing-file, and
unparseable-file cases. Reuse the existing root and mockConfigPath setup, and
assert the function’s expected boolean result for each scenario.
In `@tests/setting.test.ts`:
- Around line 79-95: Add an assertion in the writeSettings tests that the
sibling tools.rootFolder value remains present and unchanged after writeSettings
deletes or omits tools.ckbTui, covering both the default-version and
differing-version cases as appropriate.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: f28a43f8-70d2-4d52-bb5f-19a1e03db4f9
📒 Files selected for processing (27)
.github/workflows/test.ymlCHANGELOG.mdREADME.mdpackage.jsonscripts/legacy-data-test.shsrc/cfg/setting.tssrc/cli.tssrc/cmd/logs.tssrc/cmd/node.tssrc/cmd/status.tssrc/devnet/log-file.tssrc/devnet/log-subscription.tssrc/node/init-chain.tssrc/tools/ckb-tui.tssrc/tools/proxy-events.tssrc/tools/rpc-proxy.tstests/ckb-tui-checksum.test.tstests/ckb-tui-install.test.tstests/init-chain.test.tstests/log-subscription.test.tstests/logs-command.test.tstests/logs.test.tstests/node-quiet-mode.test.tstests/node-supervisor.test.tstests/node-terminal-rpc.test.tstests/proxy-events.test.tstests/setting.test.ts
| let lines = readLogTail(filePath, tail); | ||
| const scriptOnly = target === 'script'; | ||
| if (scriptOnly) lines = filterLinesByTarget(lines, SCRIPT_LOG_TARGET); | ||
| if (options.grep) lines = grepLines(lines, options.grep); | ||
| for (const line of lines) logger.info(line); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
offckb logs script can print nothing because the filter runs after the tail.
readLogTail returns the last tail lines of run.log, then filterLinesByTarget keeps only ckb-script entries. A devnet node writes many non-script lines per second. So the last 100 lines usually contain no script entry, and the command prints an empty result even though script output exists earlier in the file.
Read a larger window when a target filter is active, then trim to tail after filtering.
🐛 Proposed fix
export function showLogs(target: LogTarget, options: LogsOptions, settings: Settings, logger: UnifiedLogger): void {
const filePath = resolveLogPath(target, settings);
const tail = options.tail ?? DEFAULT_TAIL;
-
- let lines = readLogTail(filePath, tail);
const scriptOnly = target === 'script';
- if (scriptOnly) lines = filterLinesByTarget(lines, SCRIPT_LOG_TARGET);
+ // Script entries are sparse in run.log, so scan a wider window and trim
+ // back to `tail` after filtering.
+ const scanWindow = scriptOnly ? tail * 100 : tail;
+
+ let lines = readLogTail(filePath, scanWindow);
+ if (scriptOnly) lines = filterLinesByTarget(lines, SCRIPT_LOG_TARGET).slice(-tail);
if (options.grep) lines = grepLines(lines, options.grep);
for (const line of lines) logger.info(line);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| let lines = readLogTail(filePath, tail); | |
| const scriptOnly = target === 'script'; | |
| if (scriptOnly) lines = filterLinesByTarget(lines, SCRIPT_LOG_TARGET); | |
| if (options.grep) lines = grepLines(lines, options.grep); | |
| for (const line of lines) logger.info(line); | |
| const scriptOnly = target === 'script'; | |
| // Script entries are sparse in run.log, so scan a wider window and trim | |
| // back to `tail` after filtering. | |
| const scanWindow = scriptOnly ? tail * 100 : tail; | |
| let lines = readLogTail(filePath, scanWindow); | |
| if (scriptOnly) lines = filterLinesByTarget(lines, SCRIPT_LOG_TARGET).slice(-tail); | |
| if (options.grep) lines = grepLines(lines, options.grep); | |
| for (const line of lines) logger.info(line); |
🤖 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/cmd/logs.ts` around lines 32 - 36, Adjust the log retrieval flow around
readLogTail and filterLinesByTarget so target-filtered requests read a
sufficiently large window before filtering, then limit the filtered results to
the requested tail count. Preserve the existing grep behavior and ensure
unfiltered logs continue using the current tail semantics.
| export function logsCommand(target: string | undefined, options: LogsOptions): void { | ||
| const resolved: LogTarget = LOG_TARGETS.includes(target as LogTarget) ? (target as LogTarget) : 'node'; | ||
| showLogs(resolved, options, readSettings(), defaultLogger); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Reject an unknown log target instead of falling back to node.
logsCommand maps any unrecognized target to 'node'. A typo such as offckb logs scrpit then prints the node log with no warning, and the user reads the wrong output. Validate the argument and report the accepted values.
🐛 Proposed fix
export function logsCommand(target: string | undefined, options: LogsOptions): void {
- const resolved: LogTarget = LOG_TARGETS.includes(target as LogTarget) ? (target as LogTarget) : 'node';
+ if (target != null && !LOG_TARGETS.includes(target as LogTarget)) {
+ throw new Error(`Unknown log target "${target}". Use one of: ${LOG_TARGETS.join(', ')}.`);
+ }
+ const resolved: LogTarget = (target as LogTarget) ?? 'node';
showLogs(resolved, options, readSettings(), defaultLogger);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export function logsCommand(target: string | undefined, options: LogsOptions): void { | |
| const resolved: LogTarget = LOG_TARGETS.includes(target as LogTarget) ? (target as LogTarget) : 'node'; | |
| showLogs(resolved, options, readSettings(), defaultLogger); | |
| export function logsCommand(target: string | undefined, options: LogsOptions): void { | |
| if (target != null && !LOG_TARGETS.includes(target as LogTarget)) { | |
| throw new Error(`Unknown log target "${target}". Use one of: ${LOG_TARGETS.join(', ')}.`); | |
| } | |
| const resolved: LogTarget = (target as LogTarget) ?? 'node'; | |
| showLogs(resolved, options, readSettings(), defaultLogger); |
🤖 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/cmd/logs.ts` around lines 54 - 56, Update logsCommand to reject
unrecognized target values instead of defaulting them to 'node'. Validate the
provided target against LOG_TARGETS, report an error that includes the accepted
values, and only call showLogs for valid targets or when the target is omitted
and should use the default.
| const onChange = (curr: fs.Stats, prev: fs.Stats) => { | ||
| if (curr.size < prev.size || curr.size < offset) { | ||
| // Truncated or rotated: restart from the beginning. | ||
| offset = 0; | ||
| partial = ''; | ||
| decoder = new TextDecoder('utf-8'); | ||
| } | ||
| if (curr.size === offset) return; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Rotation is modeled as a size decrease in followLogFile and in its test. fs.watchFile polls a path, so a rotated log is a new inode at the same path. The shared root cause is that both the implementation and the test treat rotation as a shrinking file, which misses a replacement file that is already larger than the previous one.
src/devnet/log-file.ts#L124-L131: addcurr.ino !== prev.inoto the reset condition, next to the existing size checks.tests/logs.test.ts#L197-L201: replace the in-placefs.writeFileSynctruncation with afs.renameSyncofrun.logfollowed by writing a larger replacement file, then assert that the new content is emitted from offset 0.
📍 Affects 2 files
src/devnet/log-file.ts#L124-L131(this comment)tests/logs.test.ts#L197-L201
🤖 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/devnet/log-file.ts` around lines 124 - 131, Update followLogFile’s
onChange reset condition in src/devnet/log-file.ts:124-131 to also reset when
curr.ino differs from prev.ino, alongside the existing size checks. Update
tests/logs.test.ts:197-201 to simulate rotation by renaming run.log, writing a
larger replacement at the original path, and asserting the replacement content
is emitted from offset 0.
| export function supportsTerminalRpcModule(ckbVersion: string | null | undefined): boolean { | ||
| if (ckbVersion == null || !semver.valid(ckbVersion)) return true; | ||
| return semver.gte(ckbVersion, TERMINAL_RPC_MIN_CKB_VERSION); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
semver satisfies ">=1.2.3-0" prerelease range behavior
💡 Result:
In the npm semver implementation, the range >=1.2.3-0 is designed to explicitly include prerelease versions [1][2]. By default, the semver library excludes prerelease versions from range matching unless the range itself contains a prerelease tag [3][4]. This design ensures that users who have not explicitly opted into prerelease versions are not accidentally upgraded to unstable builds [3][5]. When you use the comparator >=1.2.3-0: 1. Opt-in Behavior: Because the range includes a prerelease identifier (-0), the semver matcher enables prerelease matching for the associated [major, minor, patch] tuple (in this case, 1.2.3) [3][4]. 2. The -0 suffix: The identifier -0 is used as a convention because it is the lowest possible prerelease version [1]. By setting the bound to 1.2.3-0, you instruct the system to include 1.2.3 and any of its subsequent prerelease versions (e.g., 1.2.3-alpha.1, 1.2.3-beta.2) [1]. 3. Range Matching: This comparator will match 1.2.3, all prerelease versions of 1.2.3 that are greater than or equal to 1.2.3-0, and all normal (non-prerelease) versions greater than 1.2.3 [6][1][7]. If you need to include all prerelease versions regardless of the default safety behavior, you can also use the includePrerelease flag (e.g., setting { includePrerelease: true } in the options object) when calling semver functions, which suppresses the requirement for an explicit prerelease tag in the range [3][4][5].
Citations:
- 1: https://github.com/WalkerCodeRanger/semver-docs/blob/master/v2.3.x/apidoc/Semver(conceptual).md
- 2: https://docs.npmx.dev/guide/semver-ranges
- 3: https://github.com/isaacs/node-semver/
- 4: https://docs.npmjs.com/cli/v6/using-npm/semver/
- 5: https://raw.githubusercontent.com/npm/node-semver/master/README.md
- 6: https://registry.npmjs.org/semver
- 7: https://github.com/npm/node-semver/blob/v7.6.3/README.md
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== files =="
git ls-files | rg '(^|/)init-chain\.(ts|test\.ts)$|package(-lock|)\.json$|tsconfig' || true
echo
echo "== init-chain relevant =="
if [ -f src/node/init-chain.ts ]; then
nl -ba src/node/init-chain.ts | sed -n '1,130p'
fi
echo
echo "== test references =="
if [ -f tests/init-chain.test.ts ]; then
rg -n "supportsTerminalRpcModule|TERMINAL_RPC_MIN_CKB_VERSION|0\.205\.0|226" tests/init-chain.test.ts -C 3 || true
fi
echo
echo "== package semver version =="
node - <<'JS'
const fs = require('fs');
for (const name of ['package.json','package-lock.json','pnpm-lock.yaml','yarn.lock']) {
if (fs.existsSync(name)) console.log(name, JSON.parse(fs.readFileSync(name,'utf8')).dependencies?.semver || JSON.parse(fs.readFileSync(name,'utf8')).devDependencies?.semver || 'missing top-level');
}
try {
const p = require('/tmp/semver-behavior');
} catch (e) {
console.log('node_modules semver availability:', e.code || e.message);
}
try {
const semver = require('semver');
console.log('semver loaded', semver.clean(process.versions.node)+"?");
const pairs = [
['0.205.0-rc1','0.205.0'],
['0.205.0-0','0.205.0'],
['0.205.0','0.205.0'],
['0.204.9','0.205.0'],
['0.205.1','0.205.0']
];
for (const [a,b] of pairs) {
console.log(a, 'gte', b, '=>', semver.gte(a,b), 'satisfies >=', b+'-0', '=>', semver.satisfies(a, '>='+b+'-0'));
}
} catch (e) {
console.log('require semver failed', e.message);
}
JSRepository: ckb-devrel/offckb
Length of output: 415
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== initial chain references =="
awk '{printf "%5d: %s\n", NR, $0}' src/node/init-chain.ts | sed -n '1,130p'
echo
echo "== test references =="
if [ -f tests/init-chain.test.ts ]; then
awk '{printf "%5d: %s\n", NR, $0}' tests/init-chain.test.ts | sed -n '210,235p'
rg -n "supportsTerminalRpcModule|TERMINAL_RPC_MIN_CKB_VERSION|0\.205\.0|226" tests/init-chain.test.ts -C 3 || true
fi
echo
echo "== package semver and runtime behavior =="
if [ -f package.json ]; then
node - <<'JS'
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('package.json','utf8'));
console.log(JSON.stringify({
dep: pkg.dependencies?.semver,
devDep: pkg.devDependencies?.semver
}));
JS
fi
if command -v node >/dev/null 2>&1; then
node - <<'JS'
try {
const semver = require('semver');
console.log('semver version', semver.SEMVER_SPEC_VERSION || 'unknown');
const pairs = [
['0.205.0-rc1','0.205.0'],
['0.205.0-0','0.205.0'],
['0.205.0','0.205.0'],
['0.204.9','0.205.0'],
['0.205.1','0.205.0'],
['0.205.0-alpha','0.205.0'],
['0.205.1-alpha','0.205.0']
];
for (const [a,b] of pairs) {
console.log(`${a} gte ${b} => ${semver.gte(a,b)} saturifes >=${b}-0 => ${semver.satisfies(a, '>=' + b + '-0')}`);
}
} catch (e) {
console.log('require semver failed:', e.message);
}
JS
fiRepository: ckb-devrel/offckb
Length of output: 10227
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== package semver versions =="
node - <<'JS'
const fs = require('fs');
if (fs.existsSync('package.json')) {
const pkg = JSON.parse(fs.readFileSync('package.json','utf8'));
console.log(JSON.stringify({
dependencies_semver: pkg.dependencies && pkg.dependencies.semver,
devDependencies_semver: pkg.devDependencies &&(pkg.devDependencies.semver),
}));
}
const files = Array.from(fs.readdirSync('.'));
for (const name of ['package-lock.json','pnpm-lock.yaml','yarn.lock','npm-shrinkwrap.json']) {
if (files.includes(name)) console.log(name, 'exists');
}
JS
node - <<'JS'
try {
const semver = require('semver');
console.log('available', true, semver.version || '');
const map = {
cgtEq: String(semver.gte('0.205.0-rc1','0.205.0')),
satisfiesZero: String(semver.satisfies('0.205.0-rc1','>=0.205.0-0')),
satisfiesEq: String(semver.satisfies('0.205.0-rc1','>=0.205.0')),
satisfy02049: String(semver.satisfies('0.204.9','>=0.205.0-0')),
satisfy02050: String(semver.satisfies('0.205.0','>=0.205.0-0')),
satisfy02070: String(semver.satisfies('0.207.0','>=0.205.0-0')),
satisfiesWithPrerelease: String(semver.satisfies('0.205.0-beta.1', '>=0.205.0-beta.0')),
satisfy02050Beta: String(semver.satisfies('0.205.0-beta.1','>=0.205.0-0'))
};
console.log(JSON.stringify(map));
} catch (e) {
console.log('semver unavailable', e.code || e.message);
}
JSRepository: ckb-devrel/offckb
Length of output: 445
Allow CKB 0.205.0 prereleases in Terminal RPC gating.
semver.gte('0.205.0-rc1', '0.205.0') is false, so 0.205.0-rc* binaries are treated as pre-Terminal and have rpc.modules stripped. Use semver.satisfies(ckbVersion, '>=0.205.0-0') and update the test expectation accordingly.
🤖 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/node/init-chain.ts` around lines 74 - 77, Update
supportsTerminalRpcModule to use semver.satisfies with the range >=0.205.0-0,
while preserving the existing null and invalid-version behavior. Update the
related test expectation so 0.205.0 prerelease versions, including rc builds,
are accepted for Terminal RPC support.
| /** | ||
| * Verifies an existing on-disk binary against the configured version's | ||
| * pinned binary digest. Returns true when the binary may be kept: either it | ||
| * matches the digest, or the configured version has no pinned binary digest | ||
| * (presence-only fallback; install-time archive verification still applies). | ||
| * Missing, unreadable, or non-regular paths (e.g. a directory or FIFO) count | ||
| * as a mismatch so the reinstall flow runs instead of crashing with a raw fs | ||
| * error, blocking forever on a special file, or failing later at spawn time. | ||
| */ | ||
| private static installedBinaryMatches(binaryPath: string): boolean { | ||
| const settings = readSettings(); | ||
| const expected = KNOWN_BINARY_SHA256[settings.tools.ckbTui.version]?.[this.getAssetName()]; | ||
| try { | ||
| // Require a regular file first: opening a FIFO or device for reading | ||
| // would block indefinitely, before any digest comparison could run. | ||
| if (!fs.statSync(binaryPath).isFile()) { | ||
| return false; | ||
| } | ||
| // Metadata alone does not prove readability; an unreadable binary must | ||
| // flow into the reinstall path (which republishes with correct modes). | ||
| fs.accessSync(binaryPath, fs.constants.R_OK); | ||
| if (!expected) { | ||
| return true; | ||
| } | ||
| const actual = crypto.createHash('sha256').update(fs.readFileSync(binaryPath)).digest('hex'); | ||
| return actual === expected; | ||
| } catch { | ||
| return false; | ||
| } | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Check X_OK, not only R_OK, or correct the doc claim.
Line 116 states that the check prevents "failing later at spawn time". accessSync(binaryPath, fs.constants.R_OK) proves readability only. A binary whose mode lost the execute bit passes both the readability check and the digest check, so ensureInstalled returns it and spawnSync then fails with EACCES. Add an execute check on POSIX so such a binary flows into the reinstall path, which republishes with mode 0o755.
🛡️ Proposed fix
// Metadata alone does not prove readability; an unreadable binary must
// flow into the reinstall path (which republishes with correct modes).
fs.accessSync(binaryPath, fs.constants.R_OK);
+ // The binary is spawned directly, so a lost execute bit must also count
+ // as a mismatch. Windows reports X_OK for any readable file.
+ if (process.platform !== 'win32') {
+ fs.accessSync(binaryPath, fs.constants.X_OK);
+ }
if (!expected) {
return true;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| /** | |
| * Verifies an existing on-disk binary against the configured version's | |
| * pinned binary digest. Returns true when the binary may be kept: either it | |
| * matches the digest, or the configured version has no pinned binary digest | |
| * (presence-only fallback; install-time archive verification still applies). | |
| * Missing, unreadable, or non-regular paths (e.g. a directory or FIFO) count | |
| * as a mismatch so the reinstall flow runs instead of crashing with a raw fs | |
| * error, blocking forever on a special file, or failing later at spawn time. | |
| */ | |
| private static installedBinaryMatches(binaryPath: string): boolean { | |
| const settings = readSettings(); | |
| const expected = KNOWN_BINARY_SHA256[settings.tools.ckbTui.version]?.[this.getAssetName()]; | |
| try { | |
| // Require a regular file first: opening a FIFO or device for reading | |
| // would block indefinitely, before any digest comparison could run. | |
| if (!fs.statSync(binaryPath).isFile()) { | |
| return false; | |
| } | |
| // Metadata alone does not prove readability; an unreadable binary must | |
| // flow into the reinstall path (which republishes with correct modes). | |
| fs.accessSync(binaryPath, fs.constants.R_OK); | |
| if (!expected) { | |
| return true; | |
| } | |
| const actual = crypto.createHash('sha256').update(fs.readFileSync(binaryPath)).digest('hex'); | |
| return actual === expected; | |
| } catch { | |
| return false; | |
| } | |
| } | |
| /** | |
| * Verifies an existing on-disk binary against the configured version's | |
| * pinned binary digest. Returns true when the binary may be kept: either it | |
| * matches the digest, or the configured version has no pinned binary digest | |
| * (presence-only fallback; install-time archive verification still applies). | |
| * Missing, unreadable, or non-regular paths (e.g. a directory or FIFO) count | |
| * as a mismatch so the reinstall flow runs instead of crashing with a raw fs | |
| * error, blocking forever on a special file, or failing later at spawn time. | |
| */ | |
| private static installedBinaryMatches(binaryPath: string): boolean { | |
| const settings = readSettings(); | |
| const expected = KNOWN_BINARY_SHA256[settings.tools.ckbTui.version]?.[this.getAssetName()]; | |
| try { | |
| // Require a regular file first: opening a FIFO or device for reading | |
| // would block indefinitely, before any digest comparison could run. | |
| if (!fs.statSync(binaryPath).isFile()) { | |
| return false; | |
| } | |
| // Metadata alone does not prove readability; an unreadable binary must | |
| // flow into the reinstall path (which republishes with correct modes). | |
| fs.accessSync(binaryPath, fs.constants.R_OK); | |
| // The binary is spawned directly, so a lost execute bit must also count | |
| // as a mismatch. Windows reports X_OK for any readable file. | |
| if (process.platform !== 'win32') { | |
| fs.accessSync(binaryPath, fs.constants.X_OK); | |
| } | |
| if (!expected) { | |
| return true; | |
| } | |
| const actual = crypto.createHash('sha256').update(fs.readFileSync(binaryPath)).digest('hex'); | |
| return actual === expected; | |
| } catch { | |
| return false; | |
| } | |
| } |
🧰 Tools
🪛 ast-grep (0.45.0)
[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { spawnSync } from 'child_process';
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(detect-child-process-typescript)
[warning] 132-132: Filesystem path is not a string literal; a request-/variable-derived path can enable path traversal. Validate and normalize the path before use.
Context: fs.readFileSync(binaryPath)
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').
(detect-non-literal-fs-filename-typescript)
🤖 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/tools/ckb-tui.ts` around lines 109 - 138, Update installedBinaryMatches
to validate execute permission in addition to readability on POSIX systems,
using fs.accessSync with X_OK so binaries without the execute bit return false
and enter the reinstall path. Preserve the existing regular-file, digest, and
fallback behavior, while handling platforms where POSIX execute checks are not
applicable.
| const line = `${new Date().toISOString()} ${sanitizeEventText(text)}\n`; | ||
| if (size < 0) size = fs.existsSync(filePath) ? fs.statSync(filePath).size : 0; | ||
| if (size > 0 && size + Buffer.byteLength(line) > maxBytes) { | ||
| fs.rmSync(`${filePath}.1`, { force: true }); | ||
| fs.renameSync(filePath, `${filePath}.1`); | ||
| size = 0; | ||
| } | ||
| fs.appendFileSync(filePath, line); | ||
| size += Buffer.byteLength(line); | ||
| } catch { | ||
| // Logging must never break request forwarding. | ||
| dirReady = false; | ||
| size = -1; | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
A failed rollover stops all further event logging.
If fs.renameSync fails, the catch block drops the event and sets size = -1. The next event re-stats the file, finds it still over maxBytes, and attempts the same rename again. On Windows a rename fails while another process holds the file open, so every later event is dropped and proxy.log stops growing. The user then sees an empty offckb logs rpc with no error.
Separate the rollover from the append. If the rollover fails, keep appending.
🐛 Proposed fix
const line = `${new Date().toISOString()} ${sanitizeEventText(text)}\n`;
if (size < 0) size = fs.existsSync(filePath) ? fs.statSync(filePath).size : 0;
if (size > 0 && size + Buffer.byteLength(line) > maxBytes) {
- fs.rmSync(`${filePath}.1`, { force: true });
- fs.renameSync(filePath, `${filePath}.1`);
- size = 0;
+ try {
+ fs.rmSync(`${filePath}.1`, { force: true });
+ fs.renameSync(filePath, `${filePath}.1`);
+ size = 0;
+ } catch {
+ // Rollover failed (for example the file is locked). Keep
+ // appending so events are not lost, and retry later.
+ size = maxBytes;
+ }
}
fs.appendFileSync(filePath, line);
size += Buffer.byteLength(line);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const line = `${new Date().toISOString()} ${sanitizeEventText(text)}\n`; | |
| if (size < 0) size = fs.existsSync(filePath) ? fs.statSync(filePath).size : 0; | |
| if (size > 0 && size + Buffer.byteLength(line) > maxBytes) { | |
| fs.rmSync(`${filePath}.1`, { force: true }); | |
| fs.renameSync(filePath, `${filePath}.1`); | |
| size = 0; | |
| } | |
| fs.appendFileSync(filePath, line); | |
| size += Buffer.byteLength(line); | |
| } catch { | |
| // Logging must never break request forwarding. | |
| dirReady = false; | |
| size = -1; | |
| } | |
| const line = `${new Date().toISOString()} ${sanitizeEventText(text)}\n`; | |
| if (size < 0) size = fs.existsSync(filePath) ? fs.statSync(filePath).size : 0; | |
| if (size > 0 && size + Buffer.byteLength(line) > maxBytes) { | |
| try { | |
| fs.rmSync(`${filePath}.1`, { force: true }); | |
| fs.renameSync(filePath, `${filePath}.1`); | |
| size = 0; | |
| } catch { | |
| // Rollover failed (for example the file is locked). Keep | |
| // appending so events are not lost, and retry later. | |
| size = maxBytes; | |
| } | |
| } | |
| fs.appendFileSync(filePath, line); | |
| size += Buffer.byteLength(line); | |
| } catch { | |
| // Logging must never break request forwarding. | |
| dirReady = false; | |
| size = -1; | |
| } |
🤖 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/tools/proxy-events.ts` around lines 63 - 76, Separate rollover operations
from event appending in the logging flow around the existing fs.renameSync and
fs.appendFileSync calls. If rollover deletion or renaming fails, retain the
current log file and continue to append the event instead of entering the catch
path that drops it or leaves size invalid; update size consistently after the
append so subsequent events can retry rollover when appropriate.
| export function handleProxyRequestBody(reqData: string, ctx: ProxyEventContext): void { | ||
| if (reqData.length === 0) return; | ||
|
|
||
| try { | ||
| const jsonRpcContent = JSON.parse(reqData) as JsonRpcRequestPayload; | ||
| const method = jsonRpcContent.method; | ||
| const params = jsonRpcContent.params; | ||
| ctx.sink.debug('RPC Req: ', method); | ||
| if (typeof method === 'string') { | ||
| ctx.events.event(`request ${method}`); | ||
| } | ||
|
|
||
| if (method === 'send_transaction') { | ||
| const tx = (params as unknown[])[0]; | ||
| const txHash = ctx.hashTransaction(tx); | ||
| if (!fs.existsSync(ctx.transactionsPath)) { | ||
| fs.mkdirSync(ctx.transactionsPath, { recursive: true }); | ||
| } | ||
| const txFile = path.resolve(ctx.transactionsPath, `${txHash}.json`); | ||
| fs.writeFileSync(txFile, JSON.stringify(tx, null, 2)); | ||
| // The hash line mirrors the RPC method name on purpose: at request time | ||
| // the proxy does not yet know whether the node will accept the tx, and | ||
| // any rejection surfaces separately as an RPC error line. | ||
| ctx.sink.info(`send_transaction: ${txHash}`); | ||
| ctx.events.event(`send_transaction ${txHash}`); | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
Handle batch request payloads and non-array params.
Two gaps in this handler:
handleProxyResponseBodyaccepts an array payload (line 131), but this function does not. A JSON-RPC batch request has no top-levelmethod, so a batchedsend_transactionis never recorded and its transaction file is never written.(params as unknown[])[0]throws aTypeErrorwhenparamsis absent or not an array. The catch then reports "Error parsing JSON-RPC req content", which points at the wrong cause.
Normalize the payload to an array and guard params.
🐛 Proposed fix
export function handleProxyRequestBody(reqData: string, ctx: ProxyEventContext): void {
if (reqData.length === 0) return;
try {
- const jsonRpcContent = JSON.parse(reqData) as JsonRpcRequestPayload;
+ const parsed = JSON.parse(reqData) as JsonRpcRequestPayload | JsonRpcRequestPayload[];
+ for (const jsonRpcContent of Array.isArray(parsed) ? parsed : [parsed]) {
+ handleOneRequest(jsonRpcContent, ctx);
+ }
+ } catch (err) {
+ ctx.sink.error('Error parsing JSON-RPC req content:', (err as Error).message);
+ }
+}
+
+function handleOneRequest(jsonRpcContent: JsonRpcRequestPayload, ctx: ProxyEventContext): void {
const method = jsonRpcContent.method;
const params = jsonRpcContent.params;
ctx.sink.debug('RPC Req: ', method);
if (typeof method === 'string') {
ctx.events.event(`request ${method}`);
}
if (method === 'send_transaction') {
- const tx = (params as unknown[])[0];
+ if (!Array.isArray(params) || params.length === 0) {
+ ctx.sink.warn('send_transaction request has no params; skipping tx dump');
+ return;
+ }
+ const tx = params[0];
const txHash = ctx.hashTransaction(tx);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export function handleProxyRequestBody(reqData: string, ctx: ProxyEventContext): void { | |
| if (reqData.length === 0) return; | |
| try { | |
| const jsonRpcContent = JSON.parse(reqData) as JsonRpcRequestPayload; | |
| const method = jsonRpcContent.method; | |
| const params = jsonRpcContent.params; | |
| ctx.sink.debug('RPC Req: ', method); | |
| if (typeof method === 'string') { | |
| ctx.events.event(`request ${method}`); | |
| } | |
| if (method === 'send_transaction') { | |
| const tx = (params as unknown[])[0]; | |
| const txHash = ctx.hashTransaction(tx); | |
| if (!fs.existsSync(ctx.transactionsPath)) { | |
| fs.mkdirSync(ctx.transactionsPath, { recursive: true }); | |
| } | |
| const txFile = path.resolve(ctx.transactionsPath, `${txHash}.json`); | |
| fs.writeFileSync(txFile, JSON.stringify(tx, null, 2)); | |
| // The hash line mirrors the RPC method name on purpose: at request time | |
| // the proxy does not yet know whether the node will accept the tx, and | |
| // any rejection surfaces separately as an RPC error line. | |
| ctx.sink.info(`send_transaction: ${txHash}`); | |
| ctx.events.event(`send_transaction ${txHash}`); | |
| } | |
| export function handleProxyRequestBody(reqData: string, ctx: ProxyEventContext): void { | |
| if (reqData.length === 0) return; | |
| try { | |
| const parsed = JSON.parse(reqData) as JsonRpcRequestPayload | JsonRpcRequestPayload[]; | |
| for (const jsonRpcContent of Array.isArray(parsed) ? parsed : [parsed]) { | |
| handleOneRequest(jsonRpcContent, ctx); | |
| } | |
| } catch (err) { | |
| ctx.sink.error('Error parsing JSON-RPC req content:', (err as Error).message); | |
| } | |
| } | |
| function handleOneRequest(jsonRpcContent: JsonRpcRequestPayload, ctx: ProxyEventContext): void { | |
| const method = jsonRpcContent.method; | |
| const params = jsonRpcContent.params; | |
| ctx.sink.debug('RPC Req: ', method); | |
| if (typeof method === 'string') { | |
| ctx.events.event(`request ${method}`); | |
| } | |
| if (method === 'send_transaction') { | |
| if (!Array.isArray(params) || params.length === 0) { | |
| ctx.sink.warn('send_transaction request has no params; skipping tx dump'); | |
| return; | |
| } | |
| const tx = params[0]; | |
| const txHash = ctx.hashTransaction(tx); | |
| if (!fs.existsSync(ctx.transactionsPath)) { | |
| fs.mkdirSync(ctx.transactionsPath, { recursive: true }); | |
| } | |
| const txFile = path.resolve(ctx.transactionsPath, `${txHash}.json`); | |
| fs.writeFileSync(txFile, JSON.stringify(tx, null, 2)); | |
| // The hash line mirrors the RPC method name on purpose: at request time | |
| // the proxy does not yet know whether the node will accept the tx, and | |
| // any rejection surfaces separately as an RPC error line. | |
| ctx.sink.info(`send_transaction: ${txHash}`); | |
| ctx.events.event(`send_transaction ${txHash}`); | |
| } | |
| } |
🧰 Tools
🪛 ast-grep (0.45.0)
[warning] 104-104: Filesystem path is not a string literal; a request-/variable-derived path can enable path traversal. Validate and normalize the path before use.
Context: fs.writeFileSync(txFile, JSON.stringify(tx, null, 2))
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').
(detect-non-literal-fs-filename-typescript)
🤖 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/tools/proxy-events.ts` around lines 86 - 111, Update
handleProxyRequestBody to normalize the parsed JSON-RPC payload into a
single-item or batch array, then process each request entry so batched
send_transaction calls are recorded and persisted. Before accessing the first
parameter, validate that params is an array; skip transaction handling for
missing or non-array params instead of allowing a TypeError to be reported as a
JSON parsing failure.
| const ctx = { | ||
| sink: logger, | ||
| events, | ||
| transactionsPath: settings[network].transactionsPath, | ||
| hashTransaction: (tx: unknown) => { | ||
| const cccTx = cccA.JsonRpcTransformers.transactionTo(tx); | ||
| return cccTx.hash() as string; | ||
| }, | ||
| }; |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Verify UnifiedLogger exposes debug/warn and how levels are configured.
ast-grep outline src/util/logger.ts --items all
rg -nP --type=ts -C3 '\b(debug|warn)\s*\(' src/util/logger.ts
# Check how the log level is selected, including any verbose flag wiring.
rg -nP --type=ts -C4 "level\s*[:=]|'debug'|\"debug\"|verbose" src/util/logger.ts src/cmd/node.ts src/cli.tsRepository: ckb-devrel/offckb
Length of output: 11531
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the relevant logger initialization, ProxyEventContext, and rpc-proxy context construction.
sed -n '24,46p' src/util/logger.ts
printf '\n--- logger.ts debug/warn around processParams/output handling ---\n'
sed -n '112,155p' src/util/logger.ts
sed -n '230,264p' src/util/logger.ts
printf '\n--- logger.ts create() default config and usages ---\n'
sed -n '100,108p' src/util/logger.ts
rg -n --type=ts '\bUnifiedLogger\.create|process\.argv|LOG_LEVEL|verbose' src -C 2
printf '\n--- proxy-events context and calls ---\n'
sed -n '1,220p' src/tools/proxy-events.ts
printf '\n--- rpc-proxy relevant lines ---\n'
sed -n '1,90p' src/tools/rpc-proxy.tsRepository: ckb-devrel/offckb
Length of output: 17331
Add ProxyEventContext annotation and connect --verbose to proxy debug logging.
UnifiedLogger has debug, warn, info, and error, so it satisfies ProxyLogSink. The global logger instance still uses Winston’s default info level; proxy request logging is at ctx.sink.debug(...), so per-request “RPC Req” lines are emitted only when LOG_LEVEL=debug. Wire --verbose into UnifiedLogger/Winston for RPC proxy startup or document that --verbose affects node/miner stdout only.
🤖 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/tools/rpc-proxy.ts` around lines 15 - 23, Annotate the ctx object in the
RPC proxy setup with the ProxyEventContext type, then connect the --verbose
option to the UnifiedLogger/Winston log-level configuration so ctx.sink.debug
emits proxy request logs during verbose runs; otherwise explicitly document that
--verbose does not enable proxy debug logging.
No description provided.