Skip to content

feat(gateway): rate limiting + replay/idempotency protection for session + webhook endpoints#47

Merged
aguilar1x merged 12 commits into
PACTO-LAT:mainfrom
jean1222312432:feat/gateway-rate-limit-replay-idempotency
Jul 19, 2026
Merged

feat(gateway): rate limiting + replay/idempotency protection for session + webhook endpoints#47
aguilar1x merged 12 commits into
PACTO-LAT:mainfrom
jean1222312432:feat/gateway-rate-limit-replay-idempotency

Conversation

@jean1222312432

Copy link
Copy Markdown
Contributor

Context

The gateway is the only component that talks to the live Pacto API, holding pk_/sk_ credentials on behalf of every merchant. Before this change, POST /v1/session and the outbound webhook path had no rate limiting or replay protection, leaving them open to abuse and duplicate-processing bugs once real merchant traffic hits.

This adds three independent, independently-testable protections to services/connect-gateway, reusing the existing gateway primitives (origin middleware, webhook signature module, toGatewayErrorBody, Prisma).

What's included

1. Rate limiting on public endpoints (per publishable key)

  • In-memory sliding-window limiter (src/middleware/rate-limit.ts), configurable via RATE_LIMIT_WINDOW_MS (default 60000) and RATE_LIMIT_MAX (default 60).
  • Mounted after originValidation so it keys off the resolved ApiKey; skips /health, /admin/*, OPTIONS, and the inbound webhook path.
  • A burst beyond the limit returns 429 with a Retry-After header.

2. Replay protection for signed webhooks (new inbound receiver)

  • Extended the webhook signature scheme with an optional nonce (t=..,n=..,v1=..), backward compatible with existing outbound signing.
  • New signature-authenticated receiver POST /v1/webhooks/inbound (src/routes/inbound-webhooks.ts) that verifies the HMAC signature, rejects timestamps outside the tolerance window, and rejects reused nonces (WebhookNonce table).
  • A replayed payload (stale timestamp or reused nonce) is rejected and logged.
  • On a transient dispatch failure the nonce is rolled back so a legitimate upstream retry is not permanently dropped.

3. End-to-end idempotency

  • Idempotency-Key header honored on POST /v1/session and POST /v1/escrows (src/middleware/idempotency.ts + IdempotencyRecord).
  • Sending the same key twice returns the original result (with Idempotent-Replayed: true), without re-running the side effect. Key reuse with a different body returns 409 idempotency_key_reuse.
  • Retried upstream events are deduped by WebhookEvent.sourceEventId, so they never double-fire merchant webhooks.

Schema / config

  • One hand-authored Prisma migration adds IdempotencyRecord, WebhookNonce, and WebhookEvent.sourceEventId.
  • New env vars documented in .env.example and WEBHOOKS.md: RATE_LIMIT_WINDOW_MS, RATE_LIMIT_MAX, PACTO_WEBHOOK_SECRET, WEBHOOK_REPLAY_TOLERANCE_SECONDS.

Testing

  • npm test in services/connect-gateway: 119 tests passing (18 files). Type-check and Biome lint clean.
  • Each acceptance criterion has a dedicated test: burst → 429 + Retry-After; timestamp-outside-tolerance and reused-nonce → rejected + logged; same idempotency key → original response with the side effect running once.

Acceptance criteria

  • A burst of requests beyond the configured limit returns 429 with a Retry-After header.
  • A replayed webhook payload (same signature, past its tolerance window) is rejected and logged.
  • Sending the same idempotency key twice returns the original result, not a duplicate side effect.

Known follow-ups (non-blocking)

  • dispatchEvent event+delivery creation is not transactional (pre-existing).
  • Idempotency persists returned 4xx/5xx responses for the same key+body.
  • Concurrent inbound events sharing a sourceEventId (different nonces) can race to a 500 that self-heals on retry.

Closes #34

@aguilar1x aguilar1x left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm!

@aguilar1x
aguilar1x merged commit 7953943 into PACTO-LAT:main Jul 19, 2026
1 check 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.

feat(gateway): rate limiting + replay/idempotency protection for session + webhook endpoints

2 participants