[WSAPI-4] Port GraphQL client with fingerprint hardening #3

Merged
chris merged 13 commits from feature/WSAPI-4 into main 2026-09-09 11:14:34 -06:00
Owner

Ticket

WSAPI-4 Port GraphQL client with fingerprint hardening

Summary

  • Ports budget-tracker-api's GraphQL query engine (lib/wealthsimple/client) — query(), retry/backoff,
    once-only 401 refresh, memoized identity lookup (getIdentityId()), and the shared Relay-cursor
    paginator (collectPages()). The fingerprint-hardening the ticket describes (consistent User-Agent,
    persistent cookie jar, Origin/Referer/Accept-Language/sec-fetch-* headers, a single verified
    x-ws-profile) was already done in WSAPI-3's lib/wealthsimple/request (wsFetch) — nothing under
    request/ changed here.
  • Ports the three existing operations as typed modules: accounts (fetchAllAccountFinancials),
    credit-card (fetchCreditCardAccount), and activity (fetchActivityFeedItems), each backed by a
    GraphQL document transcribed verbatim into lib/wealthsimple/queries.ts.
  • Adds a new balances operation — per-account security/position quantities and per-account cash
    balance, which budget-tracker-api's sync never needed. Its query (FETCH_ACCOUNT_BALANCES) is
    ws-api-python's FetchAccountsWithBalance, verified against that project's get_account_balances()
    rather than reverse-engineered against the live account. sec-c-cad/sec-c-usd are Wealthsimple's
    cash pseudo-securities; positionsFor()/cashBalanceFor() split real holdings from cash.
  • Test coverage: request-shape assertions (operation name, variables, exact header maps) and
    response-parsing tests per operation, against fixtures reused from budget-tracker-api (four existing
    operations) or synthesized from the verified schema (the new balances operation, since no live capture
    exists for it).
  • Pre-PR review (two independent passes) found and fixed 12 issues before this PR: a real bug where
    x-ws-identity-id was order-dependent across operation modules (now resolved centrally in query()),
    a balances bug where cash split across two custodian accounts under-reported (summed instead of
    .find()-ed), an overly strict schema that let one odd custodian account fail an entire multi-account
    balances call, a missing empty-id-list guard, deduped a copy-pasted zod schema into
    lib/wealthsimple/schema.ts, and added the regression/edge-case test coverage those bugs exposed
    (empty connections, null/'' quantity rejection, etc).

🤖 Generated with Claude Code

https://claude.ai/code/session_01DSc34oDVEBXLz9TJWt26Nn

## Ticket [WSAPI-4](http://192.168.2.100:7123/home/browse/WSAPI-4/) Port GraphQL client with fingerprint hardening ## Summary - Ports budget-tracker-api's GraphQL query engine (`lib/wealthsimple/client`) — `query()`, retry/backoff, once-only 401 refresh, memoized identity lookup (`getIdentityId()`), and the shared Relay-cursor paginator (`collectPages()`). The fingerprint-hardening the ticket describes (consistent User-Agent, persistent cookie jar, Origin/Referer/Accept-Language/sec-fetch-* headers, a single verified `x-ws-profile`) was already done in WSAPI-3's `lib/wealthsimple/request` (`wsFetch`) — nothing under `request/` changed here. - Ports the three existing operations as typed modules: `accounts` (`fetchAllAccountFinancials`), `credit-card` (`fetchCreditCardAccount`), and `activity` (`fetchActivityFeedItems`), each backed by a GraphQL document transcribed verbatim into `lib/wealthsimple/queries.ts`. - Adds a new `balances` operation — per-account security/position quantities and per-account cash balance, which budget-tracker-api's sync never needed. Its query (`FETCH_ACCOUNT_BALANCES`) is ws-api-python's `FetchAccountsWithBalance`, verified against that project's `get_account_balances()` rather than reverse-engineered against the live account. `sec-c-cad`/`sec-c-usd` are Wealthsimple's cash pseudo-securities; `positionsFor()`/`cashBalanceFor()` split real holdings from cash. - Test coverage: request-shape assertions (operation name, variables, exact header maps) and response-parsing tests per operation, against fixtures reused from budget-tracker-api (four existing operations) or synthesized from the verified schema (the new balances operation, since no live capture exists for it). - Pre-PR review (two independent passes) found and fixed 12 issues before this PR: a real bug where `x-ws-identity-id` was order-dependent across operation modules (now resolved centrally in `query()`), a balances bug where cash split across two custodian accounts under-reported (summed instead of `.find()`-ed), an overly strict schema that let one odd custodian account fail an entire multi-account balances call, a missing empty-id-list guard, deduped a copy-pasted zod schema into `lib/wealthsimple/schema.ts`, and added the regression/edge-case test coverage those bugs exposed (empty connections, null/`''` quantity rejection, etc). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01DSc34oDVEBXLz9TJWt26Nn
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSc34oDVEBXLz9TJWt26Nn
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSc34oDVEBXLz9TJWt26Nn
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSc34oDVEBXLz9TJWt26Nn
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSc34oDVEBXLz9TJWt26Nn
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSc34oDVEBXLz9TJWt26Nn
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSc34oDVEBXLz9TJWt26Nn
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSc34oDVEBXLz9TJWt26Nn
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSc34oDVEBXLz9TJWt26Nn
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSc34oDVEBXLz9TJWt26Nn
Three of the four operation modules (activity, credit-card, balances) never called
getIdentityId() themselves, so whichever ran first in a process sent its GraphQL
request with no x-ws-identity-id header. query() now awaits getIdentityId() itself
before building headers, so every call carries it regardless of call order.

Also: adds pageInfoSchema to client/ (next to Page<T>/collectPages, ready for the
accounts/activity dedup in the next commit), fixes the "malformed envelope" test to
actually reach envelopeSchema instead of the earlier non-JSON-body guard, and adds
an empty-connection case for collectPages.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSc34oDVEBXLz9TJWt26Nn
Correctness bugs (balances/index.ts):
- cashBalanceFor summed per-currency entries with .find(), which silently dropped
  cash held in a second custodian account (sleeve) of the same account. Now sums
  via a new sumBySecurityId helper, keyed off a single CASH_SECURITY_ID_BY_CURRENCY
  map so CASH_SECURITY_IDS and cashBalanceFor can no longer drift apart.
- custodianAccountBalanceSchema required financials.balance, but the GraphQL
  fragment only selects it inside an inline type condition
  (`... on CustodianAccountFinancialsSo`). A custodian account of a different
  financials type has no balance key at all, which failed validation for the
  entire multi-account fetchAccountBalances() call. balance is now nullish, and
  flattenBalances defaults an absent balance to [].
- fetchAccountBalances([]) now returns [] without spending a real authenticated
  round-trip against the live Wealthsimple API.

Dedup (both reviewers flagged): the number-or-numeric-string coercion schema was
copy-pasted across accounts/, credit-card/, and balances/ with three near-identical
comments. Moved to a new flat lib/wealthsimple/schema.ts (numericAmountSchema),
same untested-constants-file rationale as errors.ts/queries.ts. pageInfoSchema
(identical in accounts/ and activity/) moved to client/, next to Page<T>/collectPages.

Test coverage: regression tests for the cash-summing and optional-balance-key bugs,
a test asserting null/'' quantities are rejected rather than coerced to 0, and
empty-connection cases for fetchAllAccountFinancials and fetchActivityFeedItems.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSc34oDVEBXLz9TJWt26Nn
lib/wealthsimple/CLAUDE.md said "five operations"/"five GraphQL documents" (there
are four) and "four fixture files reused from budget-tracker-api" (there are five
- only account-balances.json is new/synthetic). Also documents the new schema.ts
and client/'s pageInfoSchema. queries.ts's FETCH_ACCOUNT_BALANCES header comment
said it's called with "a single account's id", contradicting its own $ids:
[String!]! signature and fetchAccountBalances(accountIds: readonly string[])'s
actual usage.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSc34oDVEBXLz9TJWt26Nn
[WSAPI-4] Fix stale identity-header comments left over from the order-dependency fix.
All checks were successful
server / check (pull_request) Successful in 20s
371ab38a23
Three comments still described the old order-dependent x-ws-identity-id behavior
that the previous commit's query() fix replaced. Updated to reflect the new
invariant: identity is resolved centrally inside query() before any GraphQL call
goes out, so x-ws-identity-id is present unconditionally on a GraphQL request.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSc34oDVEBXLz9TJWt26Nn
chris merged commit f89348ed66 into main 2026-09-09 11:14:34 -06:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
chris/wealthsimple-api!3
No description provided.