Skip to content

Refactor/api hardening and disputes - #784

Merged
levoski1 merged 5 commits into
WHEELBACK:mainfrom
Dev-makeem:refactor/api-hardening-and-disputes
Sep 25, 2026
Merged

levoski1 merged 5 commits into
WHEELBACK:mainfrom
Dev-makeem:refactor/api-hardening-and-disputes

Conversation

@Dev-makeem

Copy link
Copy Markdown

Closes #712
Closes #713
Closes #714
Closes #715

SUMMARY

  1. Error envelope
  • Every error now comes back as { error: { code, message, details, correlationId } }, built by one central handler in app.ts.
  • Routes throw typed errors such as NotFoundError, ValidationError, ConflictError or ContractError, which live in the new lib/errors.ts, instead
    of building responses themselves.
  • Zod validation failures put field-level details in details. Soroban errors like Error(Contract, #N) become CONTRACT_ERROR with
    details.contractCode.
  • The 429 rate-limit response, unknown routes and malformed JSON use the same envelope.
  • Other fixes along the way: compliance.ts was missing its requireEnv import, and the /simulate-settlement route called requireEnv wrongly. Both made tests hang, and 3 of the tests that were failing before now pass.
  • Also updated: tests, the OpenAPI error schema, api-reference.md, error-codes.md and rate-limits.md. I also changed 3 places in the frontend that read body.error as a string; they would otherwise have shown [object Object].
  1. Security headers and body limit
  • Added helmet. The API gets a strict Content Security Policy that blocks everything by default. /api-docs gets a looser policy so Swagger UI still loads; a test checks its files come back.
  • JSON bodies are capped at 100kb. Anything larger gets a 413 in the standard envelope.
  1. CORS allowlist
  • CORS_ORIGINS is validated in lib/env.ts: only exact origins like https://app.example.com, no wildcards or paths. A bad value stops the app at startup.
  • Unknown origins, including preflight requests, get a 403. Preflight allows the Authorization and Idempotency-Key headers.
  • Requests with no Origin header, such as curl or server-to-server calls, are not affected.
  • The variable is documented in all three .env.*.example files.
  1. Dispute read endpoints
  • GET /disputes supports status (Raised/Resolved) and settlement_id filters, plus the same pagination as /invoices.
  • GET /disputes/:id returns the full dispute with vote totals, and a 404 for an unknown ID.
  • Both are in OpenAPI and covered by supertest tests.
  • Voter identities: the vote totals are public, but who voted which way is only shown to admins with a valid x-admin-key. The voters are the treasury's signing keys, and a public per-key voting record makes it easy to single out and pressure one person. Votes are still public on-chain, so this is a precaution, not real privacy. The reasoning is in the commit message and the docs, ready to paste into the PR.

Dev-makeem and others added 4 commits September 25, 2026 04:31
Every error response now uses a single envelope:

  { error: { code, message, details, correlationId } }

- Add typed errors in lib/errors.ts (ValidationError, NotFoundError,
  ConflictError, UnauthorizedError, ContractError, RateLimitError,
  ServiceMisconfiguredError, ...) and an asyncHandler wrapper so rejected
  promises reach Express's error pipeline.
- Add a central error handler plus catch-all 404 in app.ts. Legacy
  status-tagged errors are mapped to a code by status, and Soroban
  "Error(Contract, #N)" messages become CONTRACT_ERROR with
  details.contractCode.
- Zod validation (middleware/validate.ts) and the rate limiter now emit
  the envelope; field-level issues go in details, retryAfter in details.
- requireEnv throws ServiceMisconfiguredError instead of writing to res.
- Routes throw typed errors instead of building responses by hand.
- Mount correlationId middleware before express.json so body-parse
  errors also carry a correlationId.
- Fix missing requireEnv import in compliance.ts and the malformed
  requireEnv call in simulate-settlement (both previously hung with 503
  tests timing out).
- Update tests, OpenAPI ErrorResponse schema, api-reference.md,
  error-codes.md, rate-limits.md and frontend callers that read
  body.error as a string.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Add helmet for a baseline of security headers (HSTS, nosniff,
  Referrer-Policy, frame options, ...) and drop X-Powered-By.
- Use a strict CSP (default-src 'none', frame-ancestors 'none') for the
  JSON API, with a narrowly relaxed policy under /api-docs so Swagger UI
  can still load its same-origin scripts, inline styles and data: images.
- Cap express.json() at 100kb. Oversized bodies return 413 in the
  standard envelope (code PAYLOAD_TOO_LARGE, details.limitBytes) instead
  of Express's default HTML page.
- Add tests for the 413 path, header presence, and Swagger UI assets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Add CORS_ORIGINS, a comma-separated allowlist of browser origins.
  parseCorsOrigins() in lib/env.ts validates it (bare http(s) origins
  only; no paths, trailing slash or wildcards), normalises case/default
  ports, and fails app startup listing every invalid entry.
- Add middleware/cors.ts: allowlisted origins get Access-Control-*
  headers; unknown origins (including preflight) are rejected with 403
  CORS_ORIGIN_NOT_ALLOWED in the standard error envelope. Requests with
  no Origin header are unaffected.
- Preflight allows Content-Type, Authorization, Idempotency-Key,
  X-Request-Id and X-Admin-Key, and exposes X-Request-Id, Retry-After and
  X-RateLimit-* to the browser.
- CORS runs before body parsing and rate limiting.
- Document CORS_ORIGINS in .env.local/testnet/mainnet examples and
  docs/api-reference.md; add tests for allowed/blocked origins,
  preflight, and env parsing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Add GET /disputes with status (Raised | Resolved) and settlement_id
  filters, following the invoices pagination convention
  ({ data, total, page, limit, totalPages }); invalid query params get
  the standard validation envelope.
- Add GET /disputes/:id returning full details plus tallies
  (claimant/counterparty/resolution weight, threshold, vote_count,
  outcome, resolved_at). Unknown ids return 404 NOT_FOUND.
- Back disputes with an in-memory store populated by POST /disputes
  (replacing the vote-only map). Voting on an unknown dispute now
  returns 404 instead of silently creating state.
- Voter visibility: tallies are public; individual votes (signer
  identities) are returned only to admins with a valid x-admin-key.
  Signers are treasury multi-sig keys, and a public per-signer voting
  record makes it easy to single out and pressure individual signers,
  while clients only need tallies. This is defence in depth, not
  secrecy: votes remain public on-chain. An invalid admin key is a 401.
- Fix the vote enum's custom message (zod 4 uses `message`, not
  `errorMap`).
- Document in OpenAPI (Dispute/DisputeVote schemas) and
  docs/api-reference.md; add supertest coverage including 404s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@drips-wave

drips-wave Bot commented Sep 25, 2026

Copy link
Copy Markdown

@Dev-makeem Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@levoski1
levoski1 merged commit bfe50c9 into WHEELBACK:main Sep 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants