Some checks failed
CI / Frontend Lint (push) Has been cancelled
CI / Frontend Type Check (push) Has been cancelled
CI / Frontend Build (push) Has been cancelled
CI / Frontend E2E Tests (push) Has been cancelled
CI / Orchestrator Build (push) Has been cancelled
CI / Contracts Compile (push) Has been cancelled
CI / Contracts Test (push) Has been cancelled
Security Scan / Dependency Vulnerability Scan (push) Has been cancelled
Security Scan / OWASP ZAP Scan (push) Has been cancelled
45 lines
1.6 KiB
TypeScript
45 lines
1.6 KiB
TypeScript
import { query } from "../postgres";
|
|
|
|
/**
|
|
* Migration 004 — idempotency keys + replay protection (arch §13,
|
|
* §15: deterministic state transitions, idempotent event handling,
|
|
* resilience to duplicate messages).
|
|
*
|
|
* A caller supplies an `Idempotency-Key` header on POST requests.
|
|
* The server records `{ key, request_hash, response_body, status_code }`
|
|
* on first success and replays the cached response on subsequent
|
|
* requests with the same key. If the request body changes while the
|
|
* key is reused the server returns 422 with `key_reused_with_different_payload`.
|
|
*
|
|
* Scoped by `(method, path, key)` so the same key can safely appear
|
|
* across unrelated endpoints.
|
|
*
|
|
* Rows expire after 24h — enough to cover retry windows, short enough
|
|
* to keep the table bounded.
|
|
*/
|
|
export async function up() {
|
|
await query(
|
|
`CREATE TABLE IF NOT EXISTS idempotency_keys (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
method VARCHAR(8) NOT NULL,
|
|
path VARCHAR(512) NOT NULL,
|
|
key VARCHAR(255) NOT NULL,
|
|
request_hash CHAR(64) NOT NULL,
|
|
status_code INTEGER NOT NULL,
|
|
response_body JSONB NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
expires_at TIMESTAMPTZ NOT NULL DEFAULT (CURRENT_TIMESTAMP + INTERVAL '24 hours'),
|
|
UNIQUE (method, path, key)
|
|
)`,
|
|
);
|
|
|
|
await query(
|
|
`CREATE INDEX IF NOT EXISTS idx_idempotency_expires_at
|
|
ON idempotency_keys(expires_at)`,
|
|
);
|
|
}
|
|
|
|
export async function down() {
|
|
await query("DROP TABLE IF EXISTS idempotency_keys CASCADE");
|
|
}
|