PR F: Idempotency-Key + replay protection on POST /plans and /execute #10
Reference in New Issue
Block a user
Delete Branch "devin/1776876189-idempotency"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Implements step 9 from the architecture gap-analysis. Closes out arch §13 (replay protection) and §15 (idempotent event handling, resilience to duplicate messages). Stacks on PR E.
What lands
src/middleware/idempotency.tsMounted on
POST /api/plansandPOST /api/plans/:planId/execute— the two write paths. Contract:Idempotency-Keyheader → pass-through.idempotency_key_invalid. Accepted format^[A-Za-z0-9_\-:.]{8,255}$.(method, path), same body hash) → replays cachedstatus+bodywithIdempotent-Replayed: true.idempotency_key_reused— catches client bugs where a key is accidentally reused across unrelated requests.(method, path, key)so the same key can appear onPOST /plansandPOST /plans/:id/executewithout collision.res.json()is shimmed — route handlers need no changes.Migration
004_idempotency_keys.ts24h TTL — covers realistic retry windows, keeps the table bounded.
Tests
tests/unit/idempotency.test.ts— 6 cases:idempotency_key_invalidIdempotent-Replayed: trueidempotency_key_reusedVerification
Series order
A → B → C → D → E → F → G → H.
Base:
devin/1776875929-swift-gateway(PR E). Diff here is F-only.- services/notaryChain.ts: new ethers-v6 adapter speaking to the deployed NotaryRegistry.sol via CHAIN_138_RPC_URL + NOTARY_REGISTRY_ADDRESS + ORCHESTRATOR_PRIVATE_KEY. Exposes anchorPlan(plan) -> { mode, txHash, planHash, blockNumber } and finalizeAnchor(planId, success) -> { mode, txHash, receiptHash } with deterministic mock fallback when envs are absent. - services/notary.ts: refactored to delegate to notaryChain; preserves the prior signature and returns extra on-chain fields (mode, txHash, blockNumber, contractAddress) when the anchor lands. - config/env.ts: add CHAIN_138_RPC_URL, CHAIN_138_CHAIN_ID, NOTARY_REGISTRY_ADDRESS, ORCHESTRATOR_PRIVATE_KEY (all optional, validated via regex where applicable). - package.json: add ethers@^6.11.0 dependency. - tests/unit/notaryChain.test.ts: 6 tests covering deterministic hashing helpers and the mock fallback path. tsc clean. 51 tests pass (45 pre-existing + 6 new).- db/migrations/003_events.ts: append-only events table with payload_hash, prev_hash, HMAC signature, indexed by plan_id + type - services/eventBus.ts: EVENT_TYPES union (all 15 arch §7.2 categories), publish() with hash-chain + HMAC signing, verifyChain() for tamper detection, subscribe() via in-process EventEmitter - api/plans.ts: - GET /api/plans/:planId/events (?verify=1 returns chain_valid) - GET /api/plans/:planId/events/stream (SSE with history replay + live push, 15s keep-alive, clean unsubscribe on client disconnect) - index.ts: register the two new endpoints - tests/unit/eventBus.test.ts: 9 tests covering publish, hash chain, per-plan isolation, and three tamper-detection scenarios (payload, signature, prev_hash) 60 tests pass. tsc clean.Adds an Idempotency-Key middleware backed by a new idempotency_keys table. Covers arch \u00a713 security (replay protection) and \u00a715 non-functional requirement (idempotent event handling, resilience to duplicate messages). Middleware (src/middleware/idempotency.ts): - Mounted on POST /api/plans and POST /api/plans/:planId/execute. - Key format /^[A-Za-z0-9_\-:.]{8,255}$/; malformed -> 400. - On hit, replays cached status+body with Idempotent-Replayed: true. - Reuse with a different body hash -> 422 idempotency_key_reused. - Scopes by (method, path, key) so the same key is safe across unrelated endpoints. - Only 2xx is cached. Non-2xx stays retryable. - res.json() is shimmed so handlers need no changes. - Fail-open on dedup-store unavailability (warn log). Migration 004 (db/migrations/004_idempotency_keys.ts): - idempotency_keys(method, path, key, request_hash, status_code, response_body, created_at, expires_at) with UNIQUE(method,path,key) and an expires_at index. 24h TTL. Tests: tests/unit/idempotency.test.ts \u2014 6 cases covering no-header pass-through, malformed-key 400, replay on second call, 422 on body divergence, retryable non-2xx, (method,path,key) scoping. tsc clean. 80 tests pass across 7 suites.7bcc4e38c6tod3d77c9086d3d77c9086to3650415d02