Files
CurrenciCombo/orchestrator/src/db/migrations/004_idempotency_keys.ts
nsatoshi 3ef71332dc
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
PR F: Idempotency-Key + replay protection on POST /plans and /execute (#10)
2026-04-22 17:18:25 +00:00

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");
}