Skip to content

Releases: echennells/sparkbtcbot

v0.7.0

Choose a tag to compare

@github-actions github-actions released this 31 Aug 19:48

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:
    • AlreadyExists translated where the wrapper holds proof: a deduped retry on the Spark-fallback rail now throws code: "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's reused verdict, so an AlreadyExists the wrapper can't vouch for still passes through raw.
    • payAndSettle verifies the settlement proof: sha256(preimage) must equal the invoice's payment hash or it refuses to report settled — closing the replay shape where a transferId shared with a different payment returns that payment's result as this invoice's "success". The store also refuses an explicitId already 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 idForInvoice fails loud instead of silently ignoring an id in mint's seat; a broken clock is refused at construction; fs failures are framed with the store path and SPARK_LN_DEDUP_PATH rather than a bare EACCES that nudges toward SPARK_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 dedupReused so 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 that UUID must be spark-sdk's own export (instanceof check).
    • tests/check-dev-deps.js cites README (where the ignore-scripts rule actually lives, not SECURITY.md) and recommends the doctrine-preferred npm ci --ignore-scripts.

Added

  • Persisted Lightning payment dedup: one transferId per invoice, minted write-ahead (lib/transfer-ids.js, wired into SparkAgent.payLightningInvoice and everything that funnels through it — payAndSettle and the L402 flow included). Spark-sdk 0.10 made transferId the 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 explicit transferId option that disagrees with the recorded ID is refused; a store write failure throws before any money moves. Opt out with SPARK_LN_DEDUP=off; relocate with SPARK_LN_DEDUP_PATH. The retry doctrine softens accordingly: still check getLightningSendRequest before 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 throws AlreadyExists … transfer already exists (a server-side uniqueness constraint on the transfer id). The second shape is documented in references/lightning.md because it inverts naive reading: AlreadyExists from 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-sdk 0.9.0 → 0.11.0, @buildonspark/issuer-sdk 0.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 — payLightningInvoice dropped idempotencyKey for transferId. This repo never passed idempotencyKey, 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, the proto/spark subpath is intact and TreeNode is byte-identical (leaf-vault codec unaffected). 388 unit tests green; non-funded integration 14/15, the one failure being a server-side generate_deposit_address UNAVAILABLE on hosted REGTEST that reproduces identically under 0.9.0 (environmental, tracked separately). Docs picked up the three additions that matter to this skill: the transferId retry-dedup identity — including that old idempotencyKey code 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 payLightningInvoice shape trap is now documented and guarded (production incident, 2026-08-28). The raw SDK takes ONE object (wallet.payLightningInvoice({ invoice, maxFeeSats })); the SparkAgent wrapper 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 opaque Cannot 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 in references/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 and references/agent-class.md methods list flag the difference; the wrapper throws a shape-naming TypeError on the reverse mix-up (agent.payLightningInvoice({ invoice })) before any I/O, payAndSettle included; 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

Choose a tag to compare

@echennells echennells released this 26 Aug 00:26

Security

  • --ignore-scripts on every documented install path, npm ci included. npm ci runs the full install lifecycle exactly as npm install does — 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 is protobufjs, 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 clean npm ci --ignore-scripts clone 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 the sparkbtcbot <subcommand> migration and exit 1. npx resolves 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 — while npx with 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 the npm exec --no -- forms beside them, so a reader copy-pasting from the nearest example got the weaker one. The description of --no also picked up the precise wording README settled on in 072f07e: it refuses to install, though a registry metadata request may still print a 404.

  • SPARK_ACCOUNT_NUMBER is out of README's environment-variable table and out of setup's help text, which had advertised it without ever reading it. It is not a general setting: SparkAgent and every example script omit accountNumber and take the SDK's network default, and the leaf-vault CLI is its only reader — an escape hatch so a deliberately non-default wallet can still be backed up. Listed beside SPARK_PASSPHRASE it invited being set globally, which would point the recovery bundle at a different, empty wallet than the funded one. .env.example documents it correctly and is unchanged.

v0.6.2 — an npx rule that works without a prompt

Choose a tag to compare

@echennells echennells released this 26 Aug 00:26

Note: 0.6.3 corrects one line below. npm ci runs install scripts, so it needs --ignore-scripts too — the bare npm ci recommended 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 npx for a wallet command — use npm exec --no -- sparkbtcbot <command> or ./node_modules/.bin/sparkbtcbot, and never pass -y/--yes. The pinned npx --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 at npm install time, 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 both sparkbtcbot-skill and @buildonspark/spark-sdk import cleanly without install scripts.

  • cli.js argument gate no longer falls through on inherited Object properties. The dispatcher looked up COMMANDS[cmd], which walks the prototype chain, so sparkbtcbot constructor, toString, valueOf and __proto__ resolved to a truthy non-entry and reached import(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." Now Object.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.md ships 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 package sparkbtcbot; that name now resolves to the reservation stub, so it reads sparkbtcbot-skill.

v0.6.1 — re-release 0.6.0 through the pipeline, with provenance

Choose a tag to compare

@echennells echennells released this 26 Aug 00:26

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 named sparkbtcbot, and an in-repo name-reservation stub (stub/sparkbtcbot/, published separately to npm) owns that name on the registry: a wrong-directory npx 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), sparkbtcbot with 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 exported main()set-policy, reset-ledger, and leaf-vault previously executed at top level on import. Old invocations migrate mechanically: npm exec --no -- sparkbtcbot-setupnpm exec --no -- sparkbtcbot setup. The npm run script 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 the sparkbtcbot <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

Choose a tag to compare

@echennells echennells released this 26 Aug 00:26

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 to globalThis (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 flipping 0x020x01 passed 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 use npm 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/SIGHUP skipped the seed-reveal screen wipe (the exit hook doesn't run for signals), leaving the mnemonic on screen indefinitely; (8) the reveal claimed "alternate buffer wiped" unconditionally, but GNU screen ships altscreen 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, and npm@latest on Node 20 is corrected (engines are warn-only, so it installs npm 12 anyway — use npm@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 .env to drop SPARK_DAILY_BUDGET_SATS) silently restored the full budget; a cleanup command, a prompt injection, or a machine migration all failed open. Now npx sparkbtcbot-set-policy (user-run, TTY-gated) binds dailyBudgetSats into 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 run npx sparkbtcbot-reset-ledger — the passphrase-gated legitimate reset that finally makes reset distinguishable from attack (previously rm was 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 from rm to 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 setup when 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 .env and is never echoed.

v0.4.3 — --help no longer performs the default action

Choose a tag to compare

@echennells echennells released this 26 Aug 00:26

Security

  • --help no longer performs the CLI's default action. All three published CLIs ignored unrecognized arguments and fell through to their default — which for sparkbtcbot-setup is creating a wallet: an agent probing --help for usage silently bootstrapped a real, unbacked wallet (caught by QA on REGTEST before it happened on MAINNET). sparkbtcbot-leaf-vault --help would likewise have taken a snapshot, and a typo'd flag to sparkbtcbot-reveal-mnemonic fell through toward a seed reveal. All three now gate arguments before any side effect: -h/--help prints 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 --help creates nothing.

v0.4.2 — plugin path runnable

Choose a tag to compare

@echennells echennells released this 05 Aug 20:13

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

Choose a tag to compare

@echennells echennells released this 05 Aug 19:25

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

Choose a tag to compare

@echennells echennells released this 05 Aug 19:00

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 into transfer/withdraw used to throw a misleading "must be less than 2^53". A toSats() normalizer now guards every SDK-bound amount, and dry-run/live paths agree.
  • L402 challenges parse from the WWW-Authenticate header (the spec-standard location) and support the current spec's token= field — header-only servers like aperture/lightningfaucet no longer throw Invalid 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 into claimDeposit().
  • The leaf-vault no longer cries BROKEN during cooperative L1 exits. The settlement window (leaves gone from the leaf set, owned still 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 optimizerSparkAgent.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 gapfulfillInvoice now 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 keysmaxFeePct: "10%" throws instead of silently disabling the ceiling.
  • reveal-mnemonic replaces 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 audit clean 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

Choose a tag to compare

@echennells echennells released this 29 May 16:44

0.3.0 — 2026-05-29

Added

  • Opt-in outbound recipient allowlist (lib/recipients-allowlist.js). When ~/.spark/recipients.allow exists 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 exports loadRecipientsAllowlist, assertRecipientAllowed, DEFAULT_ALLOWLIST_PATH; enforced in spark-agent.js on 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-proxy remains 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 corrupt seed.enc. Exclusive-create semantics (refuses to overwrite) and 0o600 permissions are unchanged.
  • Passphrase env hygiene. loadMnemonicFromEnv() now clears SPARK_PASSPHRASE from process.env immediately after reading it, on all paths, shrinking the window it lives in process memory (debugger snapshots, child-process inheritance, crash dumps). Opt out with loadMnemonicFromEnv({ clearEnv: false }) if a later path must re-read it. Crypto primitives unchanged (scrypt N=2¹⁷, AES-256-GCM).