-- v1.0.7 item E: raw-payload audit log for every Hyperswitch webhook -- reaching our endpoint. Captures both legitimate deliveries and -- attack attempts (invalid signatures, malformed bodies) — the insert -- happens regardless of signature-valid / processing-success status, -- so a forensics query after "something weird happened last Tuesday" -- has the actual bytes to look at. -- -- Shape decisions: -- -- payload TEXT — Hyperswitch sends JSON; TEXT is readable in psql -- without base64-decoding and plenty fast at our volumes. Invalid -- UTF-8 is rejected at INSERT time — that class of "attack" is a -- grossly malformed probe where we have the header + ip + timing -- anyway, no value in storing the binary payload. -- -- signature_valid BOOLEAN — HMAC verification outcome. Partial -- index below makes "attempts with invalid signature last 24h" -- cheap for forensics. -- -- processing_result TEXT — 'ok' on successful dispatch, 'error: ' -- on processing failure (after signature was valid), 'skipped' if -- the handler declined for another reason, 'signature_invalid' if -- rejected at the signature gate. -- -- source_ip / user_agent / request_id — forensics essentials. -- request_id is captured from Hyperswitch's X-Request-Id header if -- sent, else the handler generates a UUID so every row has a value -- correlatable against VEZA's structured logs. -- -- event_type — pulled from the payload JSON via a best-effort -- extract; NULL if the payload isn't valid JSON or doesn't carry -- an event_type field. Useful for "how many dispute.* events have -- we seen this month" — item P1.6 (disputes) rides along on this -- log without needing its own handler yet. -- -- Retention: 90 days by default, swept by CleanupHyperswitchWebhookLog -- (internal/jobs). Configurable via HYPERSWITCH_WEBHOOK_LOG_RETENTION_DAYS. CREATE TABLE IF NOT EXISTS hyperswitch_webhook_log ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), received_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), payload TEXT NOT NULL, signature_valid BOOLEAN NOT NULL, signature_header TEXT, processing_result TEXT NOT NULL, event_type TEXT, source_ip TEXT, user_agent TEXT, request_id TEXT NOT NULL ); -- Received-at ordering index: "what did we receive in the last hour" -- is the single most common operational query. Cheap, indexed by -- default PK on id but adding the timestamp index keeps retention -- sweeps and forensics scans well-planned. CREATE INDEX IF NOT EXISTS idx_hyperswitch_webhook_log_received_at ON hyperswitch_webhook_log(received_at DESC); -- Partial index on invalid signatures — "show me attack attempts". -- Partial keeps the index tiny on the common case (valid sigs) and -- makes the forensics query instantaneous on the rare case. CREATE INDEX IF NOT EXISTS idx_hyperswitch_webhook_log_signature_invalid ON hyperswitch_webhook_log(received_at DESC) WHERE signature_valid = false; -- request_id is required-NOT-NULL at the column level, so an index -- on it is just for "correlate this Veza log line with the webhook -- row". Non-unique because retries with the same request_id could -- land multiple rows (e.g., Hyperswitch redelivers a webhook). CREATE INDEX IF NOT EXISTS idx_hyperswitch_webhook_log_request_id ON hyperswitch_webhook_log(request_id);