Skip to content

Release: develop -> main - #4546

Merged
TaprootFreak merged 1 commit into
mainfrom
develop
Jul 31, 2026
Merged

Release: develop -> main#4546
TaprootFreak merged 1 commit into
mainfrom
develop

Conversation

@github-actions

Copy link
Copy Markdown

Automatic Release PR

This PR was automatically created after changes were pushed to develop.

Commits: 1 new commit(s)

Checklist

  • Review all changes
  • Verify CI passes
  • Approve and merge when ready for production

* fix(transaction): load statement relations as separate queries

PUT /v1/transaction/:id/invoice and /receipt returned 500 for every request:
QueryFailedError "target lists can have at most 1664 entries".

getTxStatementDetails' relation tree resolves to 61 joined nodes. TypeORM 0.3
expands eager relations recursively from the root entity and one level deep for
each explicitly requested relation, so Asset ends up joined 10 times and Country
9 times — 537 columns between them. Measured against real entity metadata the
tree selects exactly 1664 columns, precisely Postgres' MaxTupleAttributeNumber,
which means a single new @column anywhere in it overflows. settlementEventId on
TransactionRequest (#4469) was that column: TransactionRequest appears once in
the tree, taking it to 1665 and failing deterministically.

Pass relationLoadStrategy 'query' so each relation is fetched by its own query
instead of one 60-way join: 1664 columns/60 joins becomes 98 columns/2 joins.
Eager relations are preserved — under this strategy each relation becomes the
main alias of its own query, where eager expansion is recursive, so sub-queries
load strictly more than the join did (verified per branch against the metadata).

Also prune three branches the statement path never dereferences (cryptoRoute,
buyFiat.sell, refReward.user) and drop the explicit userData.organization, which
is already eager and so cost a redundant round-trip under the new strategy.
Under 'query' an unread branch costs a dependent query rather than just join
columns; pruning also leaves the tree at 1219 columns as a plain join, so the
fix no longer depends on the strategy alone.

* fix(payment-link): reject non-integer ids before they reach Postgres

GET /v1/paymentLink/payment, /v1/plp and /v1/paymentLink/recipient returned 500
with QueryFailedError "invalid input syntax for type integer" for NaN, Infinity,
1.9 and 1e+21. All three are reachable without authentication, and the filter
echoes the raw Postgres message — including the parameter — back to the caller.

The cause is coercing a request string with !isNaN(+x) or Number.isInteger(+x)
and handing the result to a query. Neither is sufficient: +'Infinity' is
Infinity, Number.isInteger(1e21) is true, and +'abc' is NaN, all of which pg
serialises straight into an int4 comparison.

Add Util.isDbId — digits only, within 1..2147483647 — as the single predicate,
and apply it where each value is actually consumed:

- deposit-route.service.getPaymentRoute: a non-id is a route label, so it now
  goes to the label lookup instead of the id branch (/paymentLink/recipient).
- payment-link.service.createInvoice: validated at the point of use rather than
  on the DTO, because `route` wins over `routeId` and the `r` alias is resolved
  after validation — so a junk routeId is only an error when it is the value
  actually used (/paymentLink/payment, /plp).
- payment-link.controller.parseLinkId: linkId is one of several optional lookup
  keys, so a malformed value is dropped and the request still resolves via
  externalLinkId/externalPaymentId/key. That is what the falsy NaN did
  implicitly; the point is only to keep the value out of SQL.
- generateOcpStickers: the ids list is the primary selector, so it still 400s,
  but on the correct predicate — Number.isInteger accepted 1e+21 and 2147483648
  and 500'd on an endpoint anonymous callers reach with no route knowledge.
- transaction.controller: the same coercion on :id/invoice and :id/receipt.
  Transaction UIDs are ^T[A-Za-z0-9]{16}$, so a digit-only split is unambiguous.
  Also closes the 1e+21 hole in the existing Number.isInteger guards, which
  turned the error into a 22003 overflow rather than fixing it.

Relocate PG_INTEGER_MAX to shared and point the two custody copies of this
predicate at Util.isDbId, so the bound is expressed once rather than in three
places kept in step by a comment.

* fix(payment-link): require a usable identifier before assigning a link

Round-2 review of this branch found that the previous commit introduced a hole
in PUT /v1/paymentLink/assign, which has no auth guard.

assignPaymentLink builds `where: { id, externalId, status: UNASSIGNED }` with no
dispatch and no user scoping. Dropping a malformed linkId to undefined — correct
for the other call sites, where linkId is one of several lookup keys — left both
identifiers absent, and TypeORM omits undefined keys, so the query degenerated to
`WHERE status = 'Unassigned'` and matched an arbitrary merchant's unassigned
link, which was then re-pointed at the caller's route and returned to them.
`?linkId=abc` reached that state because the presence check tested the raw query
string rather than the parsed id. The check the previous commit removed had
blocked it by accident.

Guard in the service, where the dangerous query lives, so no caller can reach it,
and check the parsed id in the controller so the endpoint answers 400.

Also replace Util.isDbId with Util.toDbId, which returns the parsed id instead of
a boolean. Callers previously re-derived the value with `+x` after the check, and
whitespace was handled inconsistently — parseLinkId and the sticker id list
trimmed, getPaymentRoute and the transaction routes did not, so `?id=+42` (a
query string decodes `+` to a space) resolved on one endpoint and 404'd on
another. Trimming now happens once, inside the parse, and there is no boolean
form to pair with a laxer coercion.

Adds coverage for the three behaviours the review found unpinned: the assign
guard, the createInvoice routeId guard, and the pruned relation tree, which was
asserted only as expect.any(Object) so any branch could have reappeared.

* test: pin the four behaviours mutation testing found unguarded

Round-3 review mutation-tested every production change on this branch: 11 of 15
failed loudly when reverted, 4 did not.

- TransactionService.getTransactionById/ByUid forwarding relationLoadStrategy to
  the repository. The helper spec asserted the argument reaching the service, so
  the service dropping it on the floor would not have failed anything — and that
  forwarding is the whole fix for the 1664-column overflow.
- createInvoicePayment classifying the `r` alias. Reverting it to !isNaN(+r) sent
  'Infinity' and '1e+21' back down the routeId branch with nothing failing.
- The id and order-id guards in getTransaction, which the earlier commit rewrote
  from Number.isInteger to Util.toDbId with no test on either.

No production code changes.
@TaprootFreak
TaprootFreak merged commit 0ec49c7 into main Jul 31, 2026
16 checks 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.

2 participants