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.