Skip to content

feat(api): add HMAC-SHA256 webhook signature validation for incoming Stellar webhooks - #4

Open
vickydve wants to merge 9 commits into
mainfrom
feature/139-webhook-signature-validation
Open

feat(api): add HMAC-SHA256 webhook signature validation for incoming Stellar webhooks#4
vickydve wants to merge 9 commits into
mainfrom
feature/139-webhook-signature-validation

Conversation

@vickydve

Copy link
Copy Markdown
Owner

What does this PR do?

Adds POST /api/v1/webhooks/stellar to receive Stellar account/transaction event notifications, protected by HMAC-SHA256 signature verification:

  • express.json() now captures the raw request bytes (req.rawBody) via its verify callback in app.ts, so signature verification runs over exactly what the client signed, not a re-serialized copy of the parsed body.
  • New verifyWebhookSignature middleware (api/middleware/webhookSignature.ts) checks the X-Stellar-Webhook-Signature header against an HMAC-SHA256 digest of the raw body, keyed with STELLAR_WEBHOOK_SECRET, using a constant-time comparison. Fails closed with 503 if no secret is configured, 401 if the header is missing or doesn't match — signature is checked before the payload is validated.
  • New stellarWebhookSchema (Zod) validates the event payload shape after the signature passes.
  • New docs/openapi.yaml entry under a new Webhooks tag, and STELLAR_WEBHOOK_SECRET documented in .env.example.

Addresses BigNathan1/CoopLumen#170 (backlog reference BigNathan1#139).

Unrelated fix bundled in (see commit message)

While wiring an HTTP-level test for the new route, npx tsc --noEmit turned up that main currently fails to compile: api/routes/transactions.ts imports buildUnsignedPayment from contracts/transactions.ts, which a prior cleanup commit (a11fe5f) deleted as "unused dead code" without removing the only caller. This breaks every test that imports app — i.e. the whole backend test suite — regardless of this PR. Restored the minimal buildUnsignedPayment implementation so main's own POST /api/v1/transactions/unsigned endpoint (and this PR's tests) work again.

Type of change

  • Bug fix (restores contracts/transactions.ts)
  • New feature (webhook signature validation)
  • Refactor
  • Documentation
  • Tests only

Testing

  • backend/src/api/middleware/__tests__/webhookSignature.test.ts: valid signature passes, missing/invalid/wrong-secret/tampered-payload signatures rejected with 401, unconfigured secret fails closed with 503, missing raw body returns 500. 8 tests.
  • backend/src/api/routes/__tests__/webhooks.test.ts: full HTTP-level coverage via supertest — valid signed payload accepted, missing/invalid signature rejected, malformed-but-signed payload rejected 400 with field errors, signature checked before payload shape, fails closed with no secret configured. 6 tests.
  • npx jest (full suite): 150 passing, 73 skipped (gated on a live DATABASE_URL), only 1 pre-existing DB-integration failure unrelated to this change (no local Postgres in this environment).
  • npx eslint on all changed files — clean.
  • npx tsc --noEmit — clean (previously failing before the contracts/transactions.ts restoration above).

Checklist

  • Tests added/updated
  • Docs updated (CHANGELOG.md, docs/openapi.yaml, .env.example)
  • No secrets in diff
  • Rebased on upstream/main

Generated by Claude Code

…Stellar webhooks

Add POST /api/v1/webhooks/stellar to receive Stellar account/
transaction event notifications. Requests must carry an
X-Stellar-Webhook-Signature header with the hex-encoded HMAC-SHA256
digest of the raw request body, keyed with STELLAR_WEBHOOK_SECRET;
the signature is verified with a constant-time comparison before the
payload is parsed against its Zod schema, and the server fails
closed (503) when no secret is configured rather than accepting
unsigned requests.

express.json() now captures the raw request bytes via its `verify`
callback so the signature can be computed over exactly what the
client signed, not a re-serialized copy of the parsed body.

Also restores backend/src/contracts/transactions.ts, which a prior
cleanup commit deleted as unused dead code without removing its only
caller (api/routes/transactions.ts) — that left main unable to
compile or run its test suite at all.

Closes BigNathan1#170
BigNathan1 and others added 8 commits August 29, 2026 02:11
Formalizes BigNathan1 as sole maintainer with write access to main, and replaces the old maintainer-nomination path with the CONTRIBUTORS.md leaderboard for recognition.
Closes BigNathan1#153. Adds full Stellar account details endpoint (sequence, thresholds, flags, balances, signers, data) with Zod validation, exponential backoff retries on Horizon 429/503, mapped error responses, unit + testnet integration tests, and OpenAPI/CHANGELOG updates.
Closes BigNathan1#155. Adds POST /api/v1/trustlines/build for unsigned changeTrust XDR (Freighter/Albedo client-side signing), with optional custom limits, Zod validation, mapped Horizon errors, tests, and OpenAPI/CHANGELOG updates. Rebased onto main by the maintainer to resolve a trivial add/add conflict on transactions.ts against BigNathan1#586 — re-verified with tsc --noEmit and the full trustlines/accounts test suite before merge.
…elpers

Closes BigNathan1#172. Adds isTestnet()/isMainnet() boolean helpers to StellarService, with unit tests and CHANGELOG update. Rebased onto main by the maintainer to resolve a CHANGELOG.md conflict; re-verified with tsc --noEmit and the stellar.test.ts suite before merge.
Closes BigNathan1#137. Adds GET /api/v1/prices/xlm with CoinGecko/Binance/Coinbase failover, 3.5s per-provider timeout, 30s Redis caching, Zod validation, and unit/integration tests plus OpenAPI/CHANGELOG updates. Rebased onto main by the maintainer to resolve conflicts against BigNathan1#586/BigNathan1#587/BigNathan1#588 (all merged just ahead of this one) in CHANGELOG.md, routes/index.ts, and openapi.yaml; re-verified with tsc --noEmit and the full price/account/trustline test suites before merge.
Closes BigNathan1#166. Extends the Horizon error mapper with the full set of documented transaction and operation result codes, with comprehensive unit test coverage. Rebased onto main by the maintainer to resolve a CHANGELOG.md conflict; re-verified with tsc --noEmit and the horizonError test suite (63 passing) before merge.
…der concurrent requests

Closes BigNathan1#169. Adds a per-account, in-memory sequence cache that serializes concurrent access so back-to-back or concurrent submissions from the same source account each get a distinct, correctly-incremented sequence number instead of racing on a stale one. Wired into asset issuance, burn, trustline establishment, and the airdrop payment loop; invalidates and retries once on tx_bad_seq. Rebased onto main by the maintainer to resolve a CHANGELOG.md conflict; re-verified with tsc --noEmit and the sequenceCache/trustlines/stellar test suites before merge.
# Conflicts:
#	CHANGELOG.md
#	backend/src/api/routes/index.ts
#	backend/src/contracts/transactions.ts
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.

4 participants