[WSAPI-5] Internal API endpoints + shared-secret auth #4

Merged
chris merged 11 commits from feature/WSAPI-5 into main 2026-09-10 13:21:55 -06:00
Owner

Ticket

WSAPI-5 Internal API endpoints + shared-secret auth

Summary

  • Adds the five internal HTTP endpoints (GET /activity-feed, /positions, /accounts, /status,
    POST /sync/trigger) behind a new shared-secret bearer gate (bearer-auth/), all reading from Mongo
    caches (lib/activity-feed-cache, lib/account-balances-cache, lib/sync-status) rather than
    Wealthsimple live — verified in tests by stubbing fetch to throw if the GET routes ever call it.
  • POST /sync/trigger is the one endpoint that calls Wealthsimple live: a small, debounced (10s + one
    trailing run so a coalesced trigger's data still gets fetched) on-demand activity-feed sync
    (sync/activity-feed/), deliberately not a full sync engine (no mutex/rate-ceiling/retry-ladder —
    WSAPI-6 or a follow-up ticket's to add if needed).
  • A central error-handler/ maps ZodError→400, SessionDeadError→503 (so a caller triggering a sync
    against a dead Wealthsimple session gets a distinguishable status), a body-parser failure's own 4xx
    status is passed through, everything else→500.
  • lib/account-balances-cache and lib/positions/accounts treat "nothing synced yet" (WSAPI-6 hasn't
    shipped the balances sync) as a valid empty state, not an error.
  • Root and lib/wealthsimple/ CLAUDE.md updated for the new endpoints, auth gate, and architecture.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DSc34oDVEBXLz9TJWt26Nn

## Ticket [WSAPI-5](http://192.168.2.100:7123/home/browse/WSAPI-5/) Internal API endpoints + shared-secret auth ## Summary - Adds the five internal HTTP endpoints (`GET /activity-feed`, `/positions`, `/accounts`, `/status`, `POST /sync/trigger`) behind a new shared-secret bearer gate (`bearer-auth/`), all reading from Mongo caches (`lib/activity-feed-cache`, `lib/account-balances-cache`, `lib/sync-status`) rather than Wealthsimple live — verified in tests by stubbing `fetch` to throw if the GET routes ever call it. - `POST /sync/trigger` is the one endpoint that calls Wealthsimple live: a small, debounced (10s + one trailing run so a coalesced trigger's data still gets fetched) on-demand activity-feed sync (`sync/activity-feed/`), deliberately not a full sync engine (no mutex/rate-ceiling/retry-ladder — WSAPI-6 or a follow-up ticket's to add if needed). - A central `error-handler/` maps `ZodError`→400, `SessionDeadError`→503 (so a caller triggering a sync against a dead Wealthsimple session gets a distinguishable status), a body-parser failure's own 4xx status is passed through, everything else→500. - `lib/account-balances-cache` and `lib/positions`/`accounts` treat "nothing synced yet" (WSAPI-6 hasn't shipped the balances sync) as a valid empty state, not an error. - Root and `lib/wealthsimple/` `CLAUDE.md` updated for the new endpoints, auth gate, and architecture. 🤖 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
Also restores Config.databaseConnectionString/internalApiKey in afterAll
across this ticket's Mongo/app test files — Bun's test file execution order
isn't lexical, so leaving them unrestored let one file's ephemeral overrides
leak into lib/config/.test.ts's default-value assertions depending on run
order.

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
- Fix HIGH: the debounce dropped a coalesced trigger's data instead of
  deferring a trailing run for it, matching budget-tracker-api's doorbell
  debounce semantics. Moved the module to sync/activity-feed/ (orchestration,
  not a Wealthsimple operation) since the change touches its whole shape.
- error-handler now passes through a real 4xx status on the error (e.g.
  express.json()'s body-parse SyntaxError) instead of flattening to 500;
  express.json() now runs after the bearer-auth gate.
- routes/positions and routes/accounts no longer assume accountBalancesCache
  documents carry syncedAt — nothing writes that collection until WSAPI-6.
- lib/sync-status derives Job from a single JOBS array and reads both job
  documents in one query instead of two.
- lib/activity-feed-cache.set() and lib/sync-status.recordSuccess() accept an
  explicit syncedAt so one sync run writes one consistent timestamp to both.
- Exported the cache document interfaces and made their arrays readonly.
- Dropped lib/config/.test.ts's ambient-env-dependent internalApiKey-default
  assertion (same fragility as the pre-existing WS_EMAIL one).
- Rewrote root CLAUDE.md's stale "no sync engine" intro paragraph.
- Added coverage: trailing-run debounce (Bun setSystemTime), error-handler's
  async-throw path and 4xx passthrough, routes/sync's SessionDeadError->503
  and generic-failure->500 paths plus "debounced call leaves syncStatus
  untouched", bearer-auth's trailing-slash/case-insensitive exemption and
  same-length wrong-token cases, routes/status's dead-session case, and exact
  ISO-string syncedAt assertions on routes/positions and routes/accounts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSc34oDVEBXLz9TJWt26Nn
- Fix HIGH: error-handler's 4xx passthrough leaked upstream Wealthsimple
  HTTP statuses (WealthsimpleApiError.status) as this API's own response
  status - a 401 from Wealthsimple was indistinguishable from this
  service's own bearer-auth gate rejecting a bad INTERNAL_API_KEY. Scoped
  the passthrough to errors that actually carry a body-parser `.type` tag
  (verified against http-errors/body-parser source), the only kind meant to
  reach it.
- Guard the trailing run's fire-and-forget call: a failing
  SyncStatus.recordFailure write inside performSync's catch block could
  previously reject uncaught, crashing the process. Wrapped in its own
  try/catch so "never throws" is actually true.
- Cancel a pending activity-feed trailing-run timer during shutdown, before
  the server and database close.
- Fixed root CLAUDE.md's two remaining descriptions of the pre-fix
  ordering (auth gate now runs before body parsing, not after).
- Documented two accepted tradeoffs in sync/activity-feed's header comment:
  the poll-based scheduler's backwards-clock-step exposure vs. an injected
  clock, and that an overlong immediate-run fetch can race its own trailing
  run (last write wins) - both deliberate for a LAN-only home service.
- Fixed a truncated sentence in create-app/index.ts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSc34oDVEBXLz9TJWt26Nn
- Fix LOW: fourxxStatus's .type-only check regressed a corrupted-gzip body
  from 400 to 500 (that failure path sets status/expose but no .type, since
  it comes from a raw zlib inflate rather than body-parser's JSON/urlencoded
  parsers). Now also accepts http-errors' expose === true as a second signal.
- Reworded fourxxStatus's comment: http-errors copies .type through from
  body-parser/raw-body rather than setting it itself, and now documents the
  .expose addition.
- Fixed root CLAUDE.md's stale error-handler/ description (still described
  the pre-fix leaky-passthrough behavior).
- Fixed an off-by-one in sync/activity-feed's overlap comment: a trailing
  run is scheduled by a second, later trigger landing inside a
  stretched-out window, not by the immediate run scheduling one against
  itself.
- Added coverage: a real malformed-JSON POST /sync/trigger through the
  fully assembled app (auth gate, express.json(), the real router, the real
  errorHandler) in create-app/.test.ts, and the corrupted-gzip-shaped
  (status+expose, no type) case in error-handler/.test.ts.
- Attempted but reverted a mock.module-based test for "recordFailure itself
  rejecting doesn't crash the caller" in sync/activity-feed/.test.ts:
  confirmed empirically that Bun's mock.module patches the shared
  @/lib/sync-status exports object retroactively and registry-wide, which a
  stray pending trailing-run timer from an earlier test picked up mid-run,
  failing an unrelated assertion. Documented the gap in a comment instead;
  the underlying try/catch fix from the prior round is unaffected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSc34oDVEBXLz9TJWt26Nn
[WSAPI-5] Trim narrated-history intros in CLAUDE.md (8d compression).
All checks were successful
server / check (pull_request) Successful in 23s
adf0933a8d
Both root and lib/wealthsimple/CLAUDE.md's intro paragraphs had accumulated
a WSAPI-3/4/5 changelog-style narration across three review rounds, largely
redundant with the Architecture tree that follows. Rewrote both as a
description of the current state (what exists, what doesn't yet, and why
sync/activity-feed lives outside lib/wealthsimple/) instead of a history of
how it got here. No other bloat found on a full reread — the rest holds up
against the 8d criteria.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSc34oDVEBXLz9TJWt26Nn
chris merged commit c6d0d8cd89 into main 2026-09-10 13:21:55 -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!4
No description provided.