Releases: echennells/sparkbtcbot
Release list
v0.7.0
Security
- Trail of Bits security-skills pass over everything since 0.6.3 (differential-review, sharp-edges, supply-chain-risk-auditor, findings verified through fp-check). Differential verdict: no Critical/High/Medium — the dedup primitive is fail-closed and safe-directional, no existing guard weakened, path traversal from attacker bolt11 ruled out empirically, supply-chain delta clean (no new install scripts, one added transitive unreachable from the Node entry, 0 advisories). The verified findings were fixed, each with a regression test:
AlreadyExiststranslated where the wrapper holds proof: a deduped retry on the Spark-fallback rail now throwscode: "PAYMENT_ALREADY_SETTLED"with the full do-not-retry rule in the message, instead of the raw gRPC error that reads as failure — gated on the store'sreusedverdict, so an AlreadyExists the wrapper can't vouch for still passes through raw.payAndSettleverifies the settlement proof:sha256(preimage)must equal the invoice's payment hash or it refuses to report settled — closing the replay shape where atransferIdshared with a different payment returns that payment's result as this invoice's "success". The store also refuses anexplicitIdalready recorded for a different invoice.- Terminal payment failure resets the dedup entry (new
store.forget()), so a still-valid invoice stays payable after a definitive failure instead of replaying it forever. - Store hardening: non-UUID ids rejected at persist AND read (a persisted one would wedge the invoice at the SDK with no pointer to the entry file); positional misuse of
idForInvoicefails loud instead of silently ignoring an id inmint's seat; a brokenclockis refused at construction; fs failures are framed with the store path andSPARK_LN_DEDUP_PATHrather than a bareEACCESthat nudges towardSPARK_LN_DEDUP=off. - Prune grace raised 1h → 24h past invoice expiry: the SDK does not refuse an expired BOLT11 client-side and a hold HTLC can pend past expiry, so early pruning could retire the one entry making a retry safe.
- Successful pays report
dedupReusedso a replayed original is distinguishable from a fresh debit, and the docs now disclose the pruning TTL, the same-store requirement behind the cross-process guarantee, and thatUUIDmust be spark-sdk's own export (instanceofcheck). tests/check-dev-deps.jscites README (where the ignore-scripts rule actually lives, not SECURITY.md) and recommends the doctrine-preferrednpm ci --ignore-scripts.
Added
- Persisted Lightning payment dedup: one
transferIdper invoice, minted write-ahead (lib/transfer-ids.js, wired intoSparkAgent.payLightningInvoiceand everything that funnels through it —payAndSettleand the L402 flow included). Spark-sdk 0.10 madetransferIdthe payment's dedup identity across every rail (Spark fallback transfer, preimage swap, SSP admission): a retry reusing it cannot produce a second payment. But the SDK mints a fresh one per call, so the guarantee evaporated exactly when it was needed — a crash or timeout between "request sent" and "outcome known" left no safe retry, which is what forced the never-retry-pay doctrine. The wrapper now persists the ID before the first attempt (one file per invoice under~/.spark/ln-dedup/, keyed by payment hash, published with atomic-file's exclusive link so concurrent racers — even across processes — converge on one ID), and any later attempt at the same invoice reuses it. Fail-closed throughout: an unreadable entry refuses to pay rather than mint a fresh ID for an invoice that may have a payment in flight; an explicittransferIdoption that disagrees with the recorded ID is refused; a store write failure throws before any money moves. Opt out withSPARK_LN_DEDUP=off; relocate withSPARK_LN_DEDUP_PATH. The retry doctrine softens accordingly: still checkgetLightningSendRequestbefore retrying, but a warranted retry through the wrapper is no longer a double-pay gamble — unless the store is gone, which the docs now name as the residual hazard. Live-validated on mainnet (2026-08-31 funded QA): duplicate pays on BOTH rails produced one debit — the Lightning rail replays the original result verbatim (same id and preimage,TRANSFER_COMPLETED), while the Spark-fallback rail throwsAlreadyExists … transfer already exists(a server-side uniqueness constraint on the transfer id). The second shape is documented inreferences/lightning.mdbecause it inverts naive reading:AlreadyExistsfrom a retry means already paid, never failed. Crash-restart ID reuse and all three fail-closed guards (conflicting explicit id, corrupt entry, opt-out) were also confirmed live.
Changed
@buildonspark/spark-sdk0.9.0 → 0.11.0,@buildonspark/issuer-sdk0.1.45 → 0.1.47 (issuer-sdk 0.1.47 pins spark-sdk 0.11.0 exactly, so the pair moves together — bumping one alone would nest a second SDK copy). Verified against the actual type-surface diff, not just the upstream changelog: only 19 declaration lines removed across the whole surface, all either internal services or the one announced break —payLightningInvoicedroppedidempotencyKeyfortransferId. This repo never passedidempotencyKey, so no code changed; the wrapper's four forwarded params (invoice,maxFeeSats,preferSpark,amountSatsToSend) all survive, every wallet method and named export the repo uses exists in 0.11.0, theproto/sparksubpath is intact andTreeNodeis byte-identical (leaf-vault codec unaffected). 388 unit tests green; non-funded integration 14/15, the one failure being a server-sidegenerate_deposit_address UNAVAILABLEon hosted REGTEST that reproduces identically under 0.9.0 (environmental, tracked separately). Docs picked up the three additions that matter to this skill: thetransferIdretry-dedup identity — including that oldidempotencyKeycode now silently gets NO dedup (references/lightning.md) — watchtower-exited-leaf recovery (references/unilateral-exit.md), and operator-enforced token allowances (references/tokens.md). The leaf-vault reach-in also has runtime proof on 0.11.0, not just the static diff: the 2026-08-31 mainnet funded QA reconstructed all 9 leaves offline to root, exit 0 — the baseline to re-verify on the next SDK bump.
Fixed
- The raw-vs-wrapper
payLightningInvoiceshape trap is now documented and guarded (production incident, 2026-08-28). The raw SDK takes ONE object (wallet.payLightningInvoice({ invoice, maxFeeSats })); theSparkAgentwrapper takes a bare BOLT11 string + options — and spark-sdk has no validation (verified in 0.9.0 and still absent in 0.11.0, the latest as of 2026-08-28), so a wrapper-style bare string at the raw layer dies on its first line with the opaqueCannot read properties of undefined (reading 'toLowerCase'). Each shape was documented correctly in its own reference, but nothing said they differ, and an agent that had seen the wrapper's examples then called the raw wallet naturally passed the string. Now: a callout inreferences/lightning.md→ Pay maps that exact error to this cause (and notes the crash is pre-flight — the invoice is unpaid, so a corrected retry is safe); the SKILL.md navigator row andreferences/agent-class.mdmethods list flag the difference; the wrapper throws a shape-namingTypeErroron the reverse mix-up (agent.payLightningInvoice({ invoice })) before any I/O,payAndSettleincluded; and eval 21 encodes the incident. The forward direction (validating inside the raw SDK) can only be fixed upstream in@buildonspark/spark-sdk.
v0.6.3 — --ignore-scripts on every install path; own every retired bin name
Security
-
--ignore-scriptson every documented install path,npm ciincluded.npm ciruns the full install lifecycle exactly asnpm installdoes — verified here with a sentinel dependency — so a lockfile-exact install is not by itself protection from a dependency that executes code at install time. In this tree that isprotobufjs, which runs before a single line of your own code is imported. README, AGENTS.md and the quick-start blocks now carry the flag throughout. Verified on the development tree as well as the consumer install: a cleannpm ci --ignore-scriptsclone runs the full 386-test suite green, so the flag costs nothing in either place. -
Every bin name this project has ever published is now project-owned on npm. Alongside
sparkbtcbot, the five names retired in 0.6.0 —sparkbtcbot-setup,sparkbtcbot-reveal-mnemonic,sparkbtcbot-leaf-vault,sparkbtcbot-set-policy,sparkbtcbot-reset-ledger— now resolve to reservation stubs that print thesparkbtcbot <subcommand>migration and exit 1.npxresolves whatever string you type as a package name, so folding a bin into a subcommand removes it from the package but not from the registry's namespace. That gap matters more here than in most projects: this skill's readers are LLM agents, the retired names were the documented form throughout 0.4.x–0.5.x and therefore sit in model weights and agent memory, and an agent working from recall never reads the SKILL.md rule that would stop it — whilenpxwith no TTY installs and executes silently, so there is no prompt to intervene either. Owning the name is the only control that acts at resolution time rather than upstream of a model's choice of what to type. Deliberately bounded to the names actually shipped and documented, not to arbitrary misspellings; a future release that retires a bin name should claim it the same way.
Changed
-
SKILL.md speaks one invocation form. Four
npx sparkbtcbot <cmd>instructions remained in user-facing lines and contradicted thenpm exec --no --forms beside them, so a reader copy-pasting from the nearest example got the weaker one. The description of--noalso picked up the precise wording README settled on in072f07e: it refuses to install, though a registry metadata request may still print a 404. -
SPARK_ACCOUNT_NUMBERis out of README's environment-variable table and out ofsetup's help text, which had advertised it without ever reading it. It is not a general setting:SparkAgentand every example script omitaccountNumberand take the SDK's network default, and theleaf-vaultCLI is its only reader — an escape hatch so a deliberately non-default wallet can still be backed up. Listed besideSPARK_PASSPHRASEit invited being set globally, which would point the recovery bundle at a different, empty wallet than the funded one..env.exampledocuments it correctly and is unchanged.
v0.6.2 — an npx rule that works without a prompt
Note: 0.6.3 corrects one line below.
npm ciruns install scripts, so it needs--ignore-scriptstoo — the barenpm cirecommended here does not protect you.
Security
-
An npx rule that holds without a prompt. Since 0.5.0 the mitigation against npx's registry fallback had been "if npx offers to install something, answer NO and stop," carried in SKILL.md, AGENTS.md and README. That rule assumes a prompt. Verified against npm 11: npx only prompts on an interactive TTY. With no TTY — a CI job, a script, or an AI agent running commands through a tool call, which is the context SKILL.md and AGENTS.md exist for — npx installs the fetched package and executes it silently, with nothing displayed and nothing to decline. It protected an interactive human and no one else while reading as general protection. The rule is now one that needs no prompt to work: never run a bare
npxfor a wallet command — usenpm exec --no -- sparkbtcbot <command>or./node_modules/.bin/sparkbtcbot, and never pass-y/--yes. The pinnednpx --package=sparkbtcbot-skill ...form is documented for a human at a terminal and explicitly not offered to agents:--package=names the owned package outright and cannot fall back to a command-name lookup, but it is still an unpinned fetch outside the lockfile. -
Install guidance uses
--ignore-scripts. One production dependency (protobufjs) executes code atnpm installtime, before the consumer's code has imported anything — the supply-chain footgun with the largest blast radius here, since it runs on the developer's machine with the agent's privileges rather than merely risking wallet funds. The flag costs nothing: the CLI resolves and runs, and bothsparkbtcbot-skilland@buildonspark/spark-sdkimport cleanly without install scripts. -
cli.jsargument gate no longer falls through on inheritedObjectproperties. The dispatcher looked upCOMMANDS[cmd], which walks the prototype chain, sosparkbtcbot constructor,toString,valueOfand__proto__resolved to a truthy non-entry and reachedimport(undefined)— a stack trace instead of the usage message, in the one gate whose own comment promises that an unknown command "cannot fall through to anything." NowObject.hasOwn(COMMANDS, cmd). Not exploitable (nothing executed, exit stayed non-zero), but it was the 0.4.3 fall-through class in miniature.
Changed
SECURITY.mdships in the npm tarball and gained a section on registry-name reservations — which names this project owns, and why owning them is what protects the npx fallback. Its scope section also named the packagesparkbtcbot; that name now resolves to the reservation stub, so it readssparkbtcbot-skill.
v0.6.1 — re-release 0.6.0 through the pipeline, with provenance
First published build of the single-CLI work. (0.6.0 was withdrawn before general availability and is not installable; 0.6.1 is the same tree, released through the tag pipeline with Sigstore provenance linking the tarball to its commit. npm treats name@version as permanently spent, so the corrected build ships under a new number rather than as a re-cut 0.6.0.)
Security
- One CLI, and its name is ours: the five per-command bins collapse into a single
sparkbtcbot <command>dispatcher. The 0.4.x–0.5.x bins (sparkbtcbot-setup,sparkbtcbot-reveal-mnemonic,sparkbtcbot-leaf-vault,sparkbtcbot-set-policy,sparkbtcbot-reset-ledger) were five unregistered npm names — each a separate landing spot for npx's registry fallback when the local bin is missing, at wallet-bootstrap or seed-reveal time. The single bin is namedsparkbtcbot, and an in-repo name-reservation stub (stub/sparkbtcbot/, published separately to npm) owns that name on the registry: a wrong-directorynpx sparkbtcbot ...lands on project-owned code that prints where you went wrong and exits 1, never a squatter's. The dispatcher also gives the ceremonies one argument gate with no default action (the 0.4.3--help-minted-a-wallet class of bug cannot recur per-script),sparkbtcbotwith no arguments prints the subcommand list instead of hitting the registry with a half-remembered name, and every command module is now inert on import with an exportedmain()—set-policy,reset-ledger, andleaf-vaultpreviously executed at top level on import. Old invocations migrate mechanically:npm exec --no -- sparkbtcbot-setup→npm exec --no -- sparkbtcbot setup. Thenpm runscript names in the cloned repo are unchanged. Breaking: the five old bin names are gone; docs-lint fails any living doc that resurrects one.
Changed
- Docs, in-code error messages (
spend-ledger,spark-agent), evals, and tests all speak thesparkbtcbot <command>form; README/SKILL.md/AGENTS.md npx guidance updated for the owned-name fallback.
v0.5.0 — persist the prompted passphrase; npm-script parity for the ceremonies
Security
-
Trail-of-Bits pass on the seed-binding work — 8 findings fixed before release. Two independent adversarial reviews (crypto/format + install/docs/reveal) ran against this batch; the crypto core survived every malleability and payload-edge-case attack, but the rollout had holes: (1)
references/agent-class.md— the "drop-in" class users actually copy — still shipped the pre-seal env-only resolver, so someone who sealed a budget and followed the doc got no budget at all; (2) a sealed seed whose policy didn't reach the agent ran silently unbound — now the plaintext version byte is checked and a sealed-but-unbound wallet throws; the context also moved toglobalThis(duplicate module copies) and a policy-less load can no longer clear an established one; (3) the seed header wasn't in the GCM AAD, so flipping0x02→0x01passed authentication and reinterpreted the payload as a bare mnemonic — v2 now authenticates the header (v1 untouched); (4)npx <cmd>does not fail closed — it fetches a registry package named after the command, and all five of our bin names were unregistered; docs now usenpm exec --no, and both agent-facing rule files say to refuse any npx install offer; (5) the ceremonies read the passphrase from.env(which sits next to the wallet), reducing "requires the passphrase" to "requires a PTY" — they now always prompt; (6) the over-budget error told sealed-policy callers to edit.env, the exact move the seal neutralizes; (7)SIGTERM/SIGHUPskipped the seed-reveal screen wipe (theexithook doesn't run for signals), leaving the mnemonic on screen indefinitely; (8) the reveal claimed "alternate buffer wiped" unconditionally, but GNU screen shipsaltscreen off— the message is now conditional and tells screen users to clear their scrollback. Also: README no longer teaches an unpinned SDK install, the pin-staleness tradeoff is stated, andnpm@lateston Node 20 is corrected (engines are warn-only, so it installs npm 12 anyway — usenpm@11). +12 regression tests. -
Seed-bound spending policy (opt-in): the budget can now be sealed inside the encrypted seed. Guard enforcement used to depend on files the agent can write —
rm ~/.spark/spend-ledger.json(or truncating it, or editing.envto dropSPARK_DAILY_BUDGET_SATS) silently restored the full budget; a cleanup command, a prompt injection, or a machine migration all failed open. Nownpx sparkbtcbot-set-policy(user-run, TTY-gated) bindsdailyBudgetSatsinto the encrypted seed payload (v2 seed format — reading it needs the passphrase, tampering fails the GCM tag, deleting it deletes the wallet: the agent can't remove the budget without removing the money), and the spend ledger becomes HMAC-signed with a seed-derived HKDF key (never the AES key). Under a bound policy, a missing / unsigned / edited ledger fails closed with instructions to runnpx sparkbtcbot-reset-ledger— the passphrase-gated legitimate reset that finally makes reset distinguishable from attack (previouslyrmwas both). A seed-bound budget wins over the env var absolutely. v1 seeds and env-var budgets are completely unchanged — no policy, no new behavior. Honest limits, documented: the bar moves fromrmto executing code, not to impossibility; replay of a validly-signed old ledger is undefendable client-side; raw-SDK calls still bypass wrapper guards. New exports:loadSeedPayload(FromEnv),validateSeedPolicy,deriveLedgerHmacKey,initSignedLedger; new CLIs:sparkbtcbot-set-policy,sparkbtcbot-reset-ledger. 16 attack-first tests (each attack proven to throw).
Changed
- Agents are explicitly told that running SETUP is allowed and expected. Live testing showed agents over-extending the reveal-mnemonic prohibition into refusing
npm run setupwhen the user asked — citing rules the docs never contained (setup has not printed or written the mnemonic in plaintext since 0.4.0). AGENTS.md and SKILL.md now carry an affirmative rule: creating the wallet on the user's behalf is the designed flow; the only secret to guard during setup is the passphrase, which goes to.envand is never echoed.
v0.4.3 — --help no longer performs the default action
Security
--helpno longer performs the CLI's default action. All three published CLIs ignored unrecognized arguments and fell through to their default — which forsparkbtcbot-setupis creating a wallet: an agent probing--helpfor usage silently bootstrapped a real, unbacked wallet (caught by QA on REGTEST before it happened on MAINNET).sparkbtcbot-leaf-vault --helpwould likewise have taken a snapshot, and a typo'd flag tosparkbtcbot-reveal-mnemonicfell through toward a seed reveal. All three now gate arguments before any side effect:-h/--helpprints usage and exits 0 (in reveal's case, before the TTY gate — usage holds no secrets), and any unknown argument fails closed with usage on stderr (exit 2). Regression tests pin that--helpcreates nothing.
v0.4.2 — plugin path runnable
Published CLIs (sparkbtcbot-setup / sparkbtcbot-reveal-mnemonic / sparkbtcbot-leaf-vault) so the Claude Code plugin path and npm consumers can run setup and the user-executed seed backup with no repo checkout. Removes the writeMnemonicBackupFile plaintext-backup export. See CHANGELOG.md.
v0.4.1 — provenance pipeline
Pipeline release — no code changes vs 0.4.0. First version published via CI with npm trusted publishing and a Sigstore provenance attestation. See CHANGELOG.md.
v0.4.0 — the live-fire release
First npm release since 0.3.0 — npm install sparkbtcbot-skill@0.4.0 — folding in the full 0.4.0 line. Also available as a Claude Code plugin (claude plugin install sparkbtcbot).
Live-validated end-to-end on real mainnet sats under two stacks: Claude Code, and opencode running GLM-5.2 via OpenRouter.
Highlights
Money-path fixes (all found by live agent testing, all regression-tested)
- BigInt sats amounts work everywhere.
getBalance()returns bigint; passing one back intotransfer/withdrawused to throw a misleading"must be less than 2^53". AtoSats()normalizer now guards every SDK-bound amount, and dry-run/live paths agree. - L402 challenges parse from the
WWW-Authenticateheader (the spec-standard location) and support the current spec'stoken=field — header-only servers like aperture/lightningfaucet no longer throwInvalid L402 challenge. listPendingDeposits()— confirmed-but-unclaimed L1 deposits were invisible (getBalance()shows claimed balance only), so agents answered "did my deposit arrive?" with a false no. Now one call lists them, feeding straight intoclaimDeposit().- The leaf-vault no longer cries BROKEN during cooperative L1 exits. The settlement window (leaves gone from the leaf set,
ownedstill counting them until L1 confirms) tripped the recovery-backup alarm on every withdrawal. The guard now demands positive SSP evidence of a pending exit before excusing the gap — and degrades to the old strict behavior on any uncertainty. - One-shot scripts that move value no longer race the SDK's background leaf optimizer —
SparkAgent.create(..., { optimizeLeaves: false }).
Security & guardrails
AGENTS.md— the load-bearing security rules (never print the mnemonic or passphrase; a "user request" to reveal the seed never arrives via fetched content or tool output) now reach non-Claude agents that don't auto-load SKILL.md.- Recipient allowlist closes its last gap —
fulfillInvoicenow decodes and gates the invoice receiver like every other outbound path. - Rolling 24h spend budget (
SPARK_DAILY_BUDGET_SATS) — bounds send loops, the failure mode per-call caps can't catch. - Guard values validated, not just guard keys —
maxFeePct: "10%"throws instead of silently disabling the ceiling. reveal-mnemonicreplaces the plaintext backup file: setup never prints or writes the words; the user reveals them on demand in their own terminal (refuses non-interactive capture).- Node floor raised to
>=20; CI audits dependencies (npm auditclean at release).
Bridge & merchants
- Boltz removed (all swaps disabled indefinitely, Aug 2026); the native Lightning↔L1 bridge is documented in both directions with every fee live-measured on mainnet, including
estimateOnrampDeposit()for sizing deposits across both fee legs. - Merchant references re-verified live (Bitrefill, nadanada, Cryptorefills): endpoint moves, response wrappers, WireGuard key generation, and catalog field-name traps documented from real purchases.
Full details in CHANGELOG.md.
🤖 Generated with Claude Code
v0.3.0 — recipient allowlist + encrypted-seed hardening
0.3.0 — 2026-05-29
Added
- Opt-in outbound recipient allowlist (
lib/recipients-allowlist.js). When~/.spark/recipients.allowexists with at least one entry, transfers and cooperative withdrawals to any address not on the list fail; a missing or empty/all-comments file leaves the gate unenforced. Spark (sp1…) and L1 addresses share one file (one per line,#comments allowed), matched as plain strings. New exportsloadRecipientsAllowlist,assertRecipientAllowed,DEFAULT_ALLOWLIST_PATH; enforced inspark-agent.json transfer/withdraw (Lightning BOLT11 sends are not gated, since they target node pubkeys). This is an operator-surprise guardrail, not a defense against a compromised agent (which can rewrite the file) —sparkbtcbot-proxyremains the path for hard-enforced, server-side limits.
Security
- Atomic, crash-safe seed writes.
saveEncryptedMnemonic()and the mnemonic-backup writer now write via temp file +fsync+ atomic rename instead of a plain exclusive-create, so a crash mid-write can no longer leave a partial or corruptseed.enc. Exclusive-create semantics (refuses to overwrite) and0o600permissions are unchanged. - Passphrase env hygiene.
loadMnemonicFromEnv()now clearsSPARK_PASSPHRASEfromprocess.envimmediately after reading it, on all paths, shrinking the window it lives in process memory (debugger snapshots, child-process inheritance, crash dumps). Opt out withloadMnemonicFromEnv({ clearEnv: false })if a later path must re-read it. Crypto primitives unchanged (scrypt N=2¹⁷, AES-256-GCM).