Repository navigation
feat(cache): add cache stats, invalidation and an opt-out flag - #670
Conversation
…v var The CLI kept three caches under ~/.nansen and offered no way to look at any of them: no size, no age, no effective TTL, and no way to tell which commands cache at all. `cache` was also missing from src/schema.json entirely, so it was invisible to `nansen schema` and to the generated shell completions. - `nansen cache stats` reports entries, bytes on disk, oldest/newest age, the effective TTL and the already-expired count for each cache (saved API responses, credit cost map, update check), as text or as an object with --json. Hits and misses are not recorded anywhere on disk, so the report says so instead of inventing a hit rate. - `nansen cache clear` takes an explicit target — responses (default), cost-map, update-check or all — and prints exactly what it removed. It only unlinks regular files in the cache it was pointed at; symlinks are skipped rather than followed, and credentials, wallets, saved quotes and config are never touched. - NANSEN_NO_CACHE=1 is a flagless equivalent of --no-cache, for wrappers that cannot change the command line. Both are a veto, never an enable. - Caching is documented once as a rule rather than per command: a `caching` section in the schema, the cache command's own help, and README. Stats are built from directory listings and stat(2) alone — no cache file is ever opened — so a cached payload, an API key, an address, an endpoint path or a cache key cannot reach the output. A test asserts it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Merge position: 9 of 10. Merge after #666, and before #669. #666 adds a drift-guard test asserting every command group in Expect textual conflicts with #668 in the Revised order for the batch: #665 → #661 → #662 → #663 → #666 → #664 → #667 → #668 → this → #669. |
pr-reviewer Summary for #a3a5253✅ No issues found The code review completed successfully with no findings. Review effort: 4/5 (Complex) SummaryThis is a well-considered and carefully hardened PR. No actionable findings were identified. The Token usage: 21,303 input, 9,653 output, 2,150,536 cache read, 70,626 cache write | Usage Guide New pushes are reviewed automatically with a 10-minute cooldown between reviews. To request a review at any time, comment |
|
@nansen-pr-reviewer re-review |
| // Windows does not define it. There, only the earlier lstat check applies; | ||
| // opening with O_RDONLY does not prevent this race. | ||
| const flags = fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0); | ||
| fd = fs.openSync(file, flags); |
There was a problem hiding this comment.
Potential file inclusion attack via reading file - high severity
If an attacker can control the input leading into the ReadFile function, they might be able to read sensitive files and launch further attacks with that information.
Show fix
Remediation: Ignore this issue only after you've verified or sanitized the input going into this function. This issue is only relevant in the backend, not in the frontend!
Reply @AikidoSec ignore: [REASON] to ignore this issue.
More info
| const flags = fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0); | ||
| fd = fs.openSync(file, flags); | ||
| if (!fs.fstatSync(fd).isFile()) return { raced: true }; | ||
| raw = fs.readFileSync(fd, 'utf8'); |
There was a problem hiding this comment.
Potential file inclusion attack via reading file - high severity
If an attacker can control the input leading into the ReadFile function, they might be able to read sensitive files and launch further attacks with that information.
Show fix
Remediation: Ignore this issue only after you've verified or sanitized the input going into this function. This issue is only relevant in the backend, not in the frontend!
Reply @AikidoSec ignore: [REASON] to ignore this issue.
More info
Summary
The CLI keeps three caches under
~/.nansenand offered no way to look at any of them — no size, no age, no effective TTL, no way to tell which commands cache at all.cachewas also missing fromsrc/schema.jsonentirely, which is why it never showed up innansen schemaor in the generated shell completions.The caches that exist (all pre-existing; this PR adds no new caching mechanism):
responses~/.nansen/cache/<digest>.json--cacheis passed--cache-ttl, default 300scost-map~/.nansen/cost-map.jsonupdate-check~/.nansen/update-check.jsonWhat
cache statsshows. Per cache: entry count, bytes on disk, age of the oldest and newest entry, the effective TTL, and how many entries are already past it — plus a total.--jsonreturns the same numbers as an object for scripting. There are no hit/miss counters: hits are decided in memory and nothing records them on disk, so the report says that outright (hit_miss_recorded: false) rather than inventing a hit rate. Adding persistent hit/miss telemetry was deliberately out of scope.Controls added.
nansen cache clearnow takes an explicit target —responses(the default, unchanged behaviour),cost-map,update-check, orall— and prints exactly what it removed. An unrecognised target is refused and removes nothing.NANSEN_NO_CACHE=1is a new flagless equivalent of the existing--no-cache, for wrappers and agent harnesses that cannot change the command line; like--no-cacheit is a veto, never an enable.Which commands cache is now documented as one rule rather than a per-command flag, because caching is a property of the request path, not of the individual command: a per-command
cached: truewould be the same sentence copied across hundreds of schema entries and would go stale the moment a command lands. Instead there is acachingsection innansen schema(scope, the controls, the TTL, the explicit never-cached list, and the three caches), the same rule innansen cachehelp, and a README section. A drift-guard test pins the documented never-cached list to the requests that actually opt out insrc/api.js, so the docs cannot silently fall behind.Privacy guarantee. Stats are built from directory listings and
stat(2)alone — no cache file is ever opened — so a cached payload, an API key, a wallet address, an endpoint path or a request parameter cannot reach the output by construction. Cache keys are digests and are not printed either; the report is aggregates only. A test asserts that none of a credential-shaped fixture, an address, an endpoint path, a payload field or any hash-shaped token appears in either rendering.Safety. Clearing only ever unlinks regular files at the three known paths:
.jsonentries directly inside the response cache directory, and the two named files. Directories and symlinks are skipped rather than followed, so a clear cannot be walked out of the cache it was pointed at.config.json,wallets/, saved quotes and saved auth are never read and never deleted — saved quotes in particular are left alone on purpose, since they expire on their own and an unexecuted quote is still spendable. Thecachecommand also no longer triggers the background update check or the cost-map refresh, which would otherwise repopulate a cache the user had just measured or cleared.Sample output (fake values):
Changes
src/cache-inspect.js(new) — reads and clears the three caches. Owns no cache of its own and never writes an entry; paths come from the modules that own them, so there is one source of truth per cache.src/cli.js—cache statsandcache clear <target>subcommands;CACHE_HELP(also whatnansen cache --helpshows);cacheadded to the--helpbanner asstats, clear;NANSEN_NO_CACHEhonoured (via an injectableenvdep onrunCLI); thecachingblock surfaced in the compactnansen schemaoutput; the cache command no longer schedules the update check or the cost-map refresh.src/schema.json— thecachecommand group with descriptions, options, returns and examples per subcommand;--cacheand--no-cacheinglobalOptionsnext to the existing--cache-ttl; a top-levelcachingsection documenting scope, controls, TTLs, the never-cached list and the three caches.src/api.js,src/cost-cache.js,src/update-check.js— export the default TTL, the two cache file paths and their TTLs. No behaviour change.src/commands/completion.js— drop the hand-writtencacheentry fromUNSCHEMA_COMMANDSnow that the schema describes it (a test asserts the two never overlap). Completions now offercache statsas well ascache clear.src/__tests__/cache-stats.test.js(new, 27 tests) — empty cache, populated cache, ages and byte counts, TTL-sensitive expiry, non-entry files ignored, the report rendering, the privacy assertion, clearing per target andall, unknown targets, the symlink guard, the command layer, the--cache/--no-cache/NANSEN_NO_CACHEmatrix, and the docs/code drift guards. Every fixture lives in a tempHOME, using the same isolation mechanism asapi-cache.test.js.README.md— a Caching section (controls, what caches, how to inspect and clear, the cache table) and the two flags in Key Options.Validation
Also smoke-tested end to end against a throwaway
HOME:cache stats,cache stats --json --pretty,cache stats --cache-ttl,cache,cache --help,cache stats --help,cache clear,cache clear all, andcache clear <bogus>(exits 1, removes nothing) — withconfig.jsonandwallets/confirmed untouched afterwards.Expected rebase points
--debugtracing) touches the sameglobalOptionsblock inschema.jsonand the cache path inapi.js. No functional overlap: it traceshttp.cache_hitto stderr for one run, which is not persistent hit/miss accounting, so nothing here duplicates it — but expect a textual conflict inglobalOptionsand in the--help/README option lists.cache stats, clearbanner line satisfies both of its rules; verified by running that PR's assertions against this branch.from_cachetelemetry) touches the cache-hit reporting incli.js, a different line from the cache options block changed here.Checklist
npm test)src/schema.jsonupdated if new commands or flags were addedREADME.mdupdated if new top-level commands or categories were added.changeset/) — only required for user-facing changes (new commands, flags, bug fixes, breaking changes)🤖 Generated with Claude Code