feat(backend): idempotency keys for payment, enrollment, and credential endpoints (#264) - #336
Closed
DanielCharis1 wants to merge 1 commit into
Closed
Conversation
…al endpoints (AetherEdu#264) - New middleware backend/src/middleware/idempotency.ts - Stripe-style Idempotency-Key header on POST/PUT/PATCH/DELETE - Redis cache + lock when available; in-process Map fallback otherwise - Replays cached responses with Idempotency-Replayed: true header - Returns 409 when a duplicate request is concurrently in flight - Per-user namespace so keys cannot replay across users - 24h TTL on cached responses, lock TTL 60s, both configurable - 5xx responses are not cached so retries remain safe - Fails open on storage errors - Releases in-flight lock on client disconnect (res.on close) so retries aren't blocked for the lock TTL when a client drops mid-request - Applies middleware to mutation endpoints: - POST /api/payments/create-payment-intent - POST /api/payments/:paymentId/refund - POST /api/enrollments - DELETE /api/enrollments/:enrollmentId - PUT /api/enrollments/:enrollmentId/progress - POST /api/credentials (new, see below) Payment gateway webhooks (/api/payments/webhook) are intentionally not wrapped; they are idempotent at the gateway level. - New credentials module to satisfy the credential-issuance requirement (one well-typed endpoint needed to apply the middleware): - backend/src/models/Credential.ts - backend/src/services/credentialService.ts (in-memory for this PR) - backend/src/controllers/CredentialsController.ts - backend/src/routes/credentialsRoutes.ts Production deployment should swap the in-memory store for the project's Mongo/Postgres layer in a follow-up. - Tests in backend/tests/middleware/idempotency.test.ts (6 cases), run via jest.isolated.config.js which bypasses the broken global setup.js that transitively imports pre-existing TS errors elsewhere. Tested locally, all 6 pass. 1. Pass-through when no key present. 2. Bad-key format -> 400. 3. Duplicate request -> replay (201 + Idempotency-Replayed: true). 4. Concurrent duplicates -> one 201, one 409 or replay, third replays. 5. 5xx response not cached, handler runs again on retry. 6. Per-user key isolation -> same key, different users, distinct results. Closes AetherEdu#264
DanielCharis1
pushed a commit
to DanielCharis1/AetherMint
that referenced
this pull request
Jul 23, 2026
Add allow-unsafe-pr-checkout: true to all actions/checkout@v4 steps in ci.yml so fork PR branches can be checked out and validated. Fork PRs (e.g. AetherEdu#336, AetherEdu#337) are currently blocked with: Refusing to check out fork pull request code from a 'pull_request_target' workflow. Security: ci.yml does not pass sensitive secrets to build steps.
DanielCharis1
pushed a commit
to DanielCharis1/AetherMint
that referenced
this pull request
Jul 23, 2026
Add allow-unsafe-pr-checkout: true to all actions/checkout@v4 steps in ci.yml so fork PR branches can be checked out and validated. Fork PRs (e.g. AetherEdu#336, AetherEdu#337) are currently blocked with: Refusing to check out fork pull request code from a 'pull_request_target' workflow. Security: ci.yml does not pass sensitive secrets to build steps.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements Stripe-style Idempotency-Key support for mutation endpoints (#264).
What ships
New middleware
backend/src/middleware/idempotency.ts:Idempotency-Keyon POST/PUT/PATCH/DELETE/^[A-Za-z0-9._:\-]{8,255}$/→ 400 on bad inputMapfallback otherwiseIdempotency-Replayed: trueon replay409 Conflictwhen a duplicate request is concurrently in flightres.on('close')so client disconnects don't block retriesMiddleware applied to mutation endpoints:
POST /api/payments/create-payment-intentPOST /api/payments/:paymentId/refundPOST /api/enrollmentsDELETE /api/enrollments/:enrollmentIdPUT /api/enrollments/:enrollmentId/progressPOST /api/credentials(new endpoint)POST /api/payments/webhookis not wrapped — payment-gateway webhooks carry their own gateway-level idempotency tokens.New credentials module (needed to provide a credential-issuance mutation endpoint that exercises the middleware):
backend/src/models/Credential.tsbackend/src/services/credentialService.ts(in-memory store; production swaps for the project's persistent layer)backend/src/controllers/CredentialsController.tsbackend/src/routes/credentialsRoutes.tsTests
backend/tests/middleware/idempotency.test.ts(6 cases):400201replay withIdempotency-Replayed: true201, one409/replay; a third request replays from cache5xxresponse is not cached → next retry runs the handler againRun locally:
jest.isolated.config.jsis included so the test bypasses the broken globalsetupFilesAfterEnv(which transitively pulls in pre-existing TS errors elsewhere in the repo).Acceptance-criteria mapping (#264)
idempotency-keyfallback on inputIdempotency-Replayed: trueNotes
backend/src/index.jsis intentionally not modified (route registration is out-of-scope here).Map. Replace with the project Mongo/Postgres layer in a follow-up.Closes #264