Skip to content

v0.17.0: payment, refund and receiver reliability fixes

Choose a tag to compare

@jakethehoffer jakethehoffer released this 19 Sep 21:38
· 6 commits to master since this release

Download status

Release complete, verified September 21, 2026. npm install ledgerly now resolves to 0.17.0. Docker ghcr.io/jakethehoffer/ledgerly:v0.17.0 is published for linux/amd64 and linux/arm64. The actual npm download and downloaded amd64 Docker image passed the checks below.

Verification

  • 912 tests, type checking, lint and build passed on Linux Node22 and24. The npm release gate passed again on Node24, and release run 35470570202 attempt 3 completed successfully.
  • Clean npm consumer install: public import, balanced payment, annual schedule, mapping launcher, and refusal of missing or incomplete receiver setup passed.
  • Downloaded npm receiver: early refund survived a process restart, protected retry succeeded after its invoice arrived, duplicate refund and invoice charge notices were counted once, and remaining deferral was 30000 cents. Health, readiness and metrics returned 200; invalid signatures and unauthorized admin calls were rejected.
  • npm registry signatures and attestations passed verification. Package provenance names the existing v0.17.0 tag at commit b1e2650.
  • Downloaded amd64 image: early refund retained across a process restart, protected retry succeeds after its invoice arrives, repeat refund and invoice charge notices do not duplicate entries, remaining deferral is 30000 cents.
  • Synthetic database made with SQLite binding11 retained its record under binding13, passed integrity checking and held an uncertain old send for review.
  • Actual server launcher, health/readiness/metrics and mapping launcher work. Build attestation verified against commit b1e2650.
  • Both architectures built and published. The arm64 download was pulled, but this verification host cannot execute ARM binaries, so arm64 runtime behavior was not checked here.
  • No live payment or accounting accounts were used. These checks do not prove every accounting case or close issue2's remaining test gap.

The release includes the payment, refund, retry, and OAuth fixes below, plus the SQLite binding update needed for current Node.

Fixed

  • Upgrade better-sqlite3 to ^13.0.3, whose N-API implementation avoids the
    native statement cleanup crash seen with the old binding on current Node 24.
    Closing test databases alone did not resolve the crash during publication.

Upgrade notes

  • Node 22 or newer is now required. The Docker image includes Node 24. Direct
    receiver installations must also update better-sqlite3 to ^13.0.3.
    This new minimum is a pre-1.0 breaking change, so it uses a minor release.
  • Includes all payment, refund, retry, and OAuth fixes listed under 0.16.0.
    Follow those database backup and reconciliation notes when upgrading from
    0.15.1 or earlier. Existing SQLite data is retained.
  • Supersedes the partial 0.16.0 release: its Docker image was published, but
    npm publication stopped at the native test crash. Existing tags are preserved.

Included reliability and security changes

[0.16.0] — 2026-09-19

Upgrade notes

  • Back up SQLite before upgrading. The first startup holds previously attempted,
    unposted sends whose outcome is uncertain. Reconcile those rows with the
    accounting provider before releasing or recreating them. This release does
    not repair entries already sent by older versions.
  • Custom Storage implementations now require inbox and
    persistRefundReversal. Use a bundled backend or implement the updated
    contract before upgrading. This is a pre-1.0 interface change.
  • Configure the accounting dispatcher before enabling the scheduler. There is
    no automatic console fallback. The receiver can collect events with
    LEDGERLY_SCHEDULER_ENABLED=false. OAuth setup also requires an admin token.
  • Failed verified webhook payloads remain in SQLite until successfully handled.
    Protect that database and its backups as payment data. Refunds of deferred
    invoices from before Ledgerly require reconciled opening balances and
    recognition history before retry.

Fixed

  • Keep deferred-invoice refunds retryable until their invoice schedule exists. Out-of-order refunds no longer reverse unearned revenue or leave the full schedule to be recognized later. Applies to both bundled storage backends and the legacy deduplicator path.
  • Count invoice payments once across charge and invoice notices, and deduplicate refunds by refund ID in every bundled storage path. Preserve FX context when reducing recognition schedules.
  • Retain failed signed webhooks for protected operator retry. Contain storage errors and keep unreadable scheduled rows for repair without blocking healthy rows.
  • Fetch complete invoice line lists and retrieve newer invoice payloads in the supported schema. Keep unsettled refunds pending instead of prematurely booking them.
  • Stop incomplete accounting setup before dispatch. Console-only posting now requires explicit opt-in, and the sample configuration starts with the scheduler disabled.
  • Hold uncertain pre-upgrade dispatch attempts for manual reconciliation instead of automatically replaying them with new request identities.
  • Give QBO sends stable request IDs and check Xero narration markers before retries, including retries after Xero's short idempotency window. Reject Xero responses that do not confirm a saved journal.

Security

  • Only the operator can connect an accounting company now.
    GET /oauth/<provider>/start had no authentication. Anyone who found the URL
    could complete consent with their own QuickBooks or Xero company: connecting
    after the operator added a second token row, which stops every dispatch, and
    connecting first made their company the only row, so the receiver would post
    the operator's journal entries — amounts and memos carrying Stripe customer,
    subscription and charge references — into a stranger's books. start is now
    gated behind LEDGERLY_ADMIN_TOKEN, and is not mounted at all when that token
    is unset. The callback cannot be gated the same way (it arrives as a redirect
    from the provider, not from the operator), so it now requires a state nonce
    that an authenticated start on this process issued, and accepts each one only
    once — a leaked or replayed consent link no longer works.

Added

  • Operator endpoints for connected companies. GET /admin/oauth lists the
    connected QBO / Xero companies (provider, tenant id, token expiry, scope —
    never the token values) and DELETE /admin/oauth/<provider>/<tenantId> removes
    one. Before this there was no supported way to clear a token row, so two rows
    for one provider stopped dispatch with no recovery short of editing storage by
    hand. The "multiple token rows" error now names these endpoints.

  • Storage.persistRefundReversal — the atomic read + deferred-first split +
    cancel + re-spread used by the refund reconciliation below, alongside the
    existing persistCreditReversal. Both bundled backends implement it. Custom
    Storage implementations must add it (pre-1.0 interface change).

Changed

  • The server CLI now exits at startup if OAuth client config is set without
    LEDGERLY_ADMIN_TOKEN
    , rather than starting with a connect flow that cannot
    be reached — the same treatment the CLI already gives other partial OAuth
    configuration. Upgrade impact: a deployment currently running OAuth client
    config with no admin token will not boot until you set one (openssl rand -base64 48), or drop the OAuth client variables and use the static-token
    dispatchers. Already-stored tokens are untouched and keep dispatching. That
    deployment is also the one exposed to the issue above, so setting the token is
    the fix either way.
  • README correction: connecting a different QBO realm or Xero org does not
    overwrite the existing token row. It adds a second row, which stops dispatch
    until one is removed.

Fixed

  • Refunding a deferred-schedule invoice no longer strands a phantom deferred
    liability.
    A cash refund against an invoice that deferred revenue was booked
    entirely to 4900 Refunds Issued, reversing revenue the ledger had never
    recognized. 2100 kept the refunded amount forever and net revenue went
    negative — refunding the unrecognized $900 of a $1,200 annual plan three months
    in left 2100 at $900 and revenue at −$600 on $300 of delivered service. The
    bundled receiver now reconciles such a refund against the schedule: it repays
    the still-deferred balance first (Dr 2100), posts to 4900 only the excess over
    all remaining deferred, and re-spreads whatever stays deferred across the
    remaining months. The cash leg, the proportional tax drain, realized FX, and the
    cumulative basis across a multi-refund charge are unchanged, so refunds of
    non-deferred invoices book exactly as before. A refund after a subscription
    cancellation now also closes out the held schedule instead of leaving it
    stranded. The pure engine keeps its stateless 4900 behaviour and its documented
    limitation.

  • A scheduled-entry race can no longer stop the webhook receiver. If a
    refund or other reconciliation changes a due row just before dispatch starts,
    the scheduler now contains that row's error and continues the batch. Temporary
    storage read failures are logged and retried on the next tick instead of
    escaping as an unhandled promise rejection.