You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.