Skip to content

F-06 (Medium): Guard the bigint↔number bridge at the storage boundary#6

Merged
adamburmister merged 2 commits into
mainfrom
fix/f06-safe-integer-guard
Jul 18, 2026
Merged

F-06 (Medium): Guard the bigint↔number bridge at the storage boundary#6
adamburmister merged 2 commits into
mainfrom
fix/f06-safe-integer-guard

Conversation

@adamburmister

Copy link
Copy Markdown
Owner

Finding

F-06 (Medium) from the ledger audit: domain math is exact bigint, but storage is not — the write path bound Number(amount.minor), and every read (including SUM() aggregates) returned a JS number re-wrapped via BigInt(...). Neither conversion errors on precision loss: Number(9007199254740993n) yields 9007199254740992 (verified in the audit, probe P6c). Above 2^53 minor units, amounts and aggregates silently corrupt.

The 2^53 ceiling ($90T at scale 2) was documented in a comment but enforced nowhere — a policy, not a control. It matters because the README explicitly recommends raising SCALE for higher-precision currencies (8 for crypto), which collapses the ceiling to ~90 million major units — well inside plausible balances. At that point balanceByType and trialBalance would drift by ±1 minor unit with no error anywhere.

Fix

Exactness is now enforced, not assumed, at the one seam where it can be lost:

  • toStorageInt(minor) (write path): throws RepositoryError beyond Number.MAX_SAFE_INTEGER instead of silently rounding. Conversions are precomputed before the insert transaction opens, so an out-of-range amount fails cleanly with nothing written.
  • fromStorageInt(value, context) (read paths — row hydration, per-account and per-type SUMs): rejects unsafe integers (silent aggregate overflow) and non-integer values (float contamination reaching the column via non-library SQL — complements the DB-level typeof(amount) = 'integer' trigger in F-04 (High) + F-14: Append-only triggers and row-validity checks #4).

Both helpers are exported for third-party Repository ports, which face the identical driver problem.

Testing

New storage-int.spec.ts (written first, watched fail): exact pass-through up to MAX_SAFE_INTEGER, loud failure at 2^53 and beyond in both directions, and float/NaN rejection on the read path.

Full suite: 83 passed. tsc --noEmit and biome check clean.

🤖 Generated with Claude Code

SqlStorage cannot bind bigint, so every amount crosses an IEEE 754
number boundary on write, read, and SUM aggregation. Number() and
BigInt() do not error on precision loss — above 2^53 minor units they
silently round, which for a ledger means corrupted money with no
signal anywhere. The ceiling (~$90T at scale 2) is documented but was
unenforced, and the README's advice to raise SCALE for high-precision
currencies shrinks it to ~90M major units at scale 8 — well inside
plausible balances.

- toStorageInt(minor): throws RepositoryError beyond 2^53 - 1 instead
  of silently rounding; applied to the write path, precomputed before
  the insert transaction opens
- fromStorageInt(value, context): rejects non-safe integers (silent
  SUM overflow) and non-integers (float contamination via non-library
  SQL) on every read path — row hydration and both SUM aggregates

Audit finding F-06 (Medium).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 28860c7fe7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/db/sqlite-storage-repository.ts
The PR states toStorageInt/fromStorageInt are exported for third-party
Repository ports, but only the internal module exported them — the package
exports map exposes only src/index.ts, so consumers could not import them.

Addresses PR #6 review (P2).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@adamburmister
adamburmister merged commit acf67a3 into main Jul 18, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant