1-- TASK-0130 — Webhooks production: encrypted raw signing secret + DLQ status 2-- + inspector index. 3-- 4-- 1. `webhooks.secretEncrypted BYTEA?` — the RAW signing secret, AES-256-GCM 5-- (MASTER_ENCRYPTION_KEY), base64(iv:tag:ciphertext). Every webhook created 6-- from now on stores its raw secret here; `secretHash` is recomputed as 7-- SHA-256(raw) and stays for the incoming-verification/legacy path. 8-- (C-13.8a: the Phase-11 rows stored the CIPHERTEXT in `secretHash`; the 9-- backfill below moves those into the proper column, and the app's 10-- resolveSigningSecret() falls back to decrypting `secretHash` directly 11-- for any row the SQL predicate misses.) 12-- 2. `WebhookDeliveryStatus` gains DEAD_LETTER — the terminal status after the 13-- final (8th) attempt fails. Kept in webhook_deliveries for the delivery 14-- inspector; a resubmit re-queues it with a fresh attempts counter. 15-- 3. `webhook_deliveries [webhookId, createdAt desc]` — inspector pagination. 16-- 17-- Backfill (best-effort, no decryption in SQL): copy a `secretHash` that is NOT 18-- a bare 64-hex-char SHA-256 into `secretEncrypted`. A bare hash cannot be 19-- recovered (its deliveries sign with the hash-derived key — documented legacy 20-- behavior); a base64 ciphertext can, and this move makes the common case hit 21-- the clean `secretEncrypted` path in the app. 22 23ALTER TYPE "WebhookDeliveryStatus" ADD VALUE IF NOT EXISTS 'DEAD_LETTER'; 24 25ALTER TABLE "webhooks" ADD COLUMN "secretEncrypted" BYTEA; 26 27UPDATE "webhooks" 28SET "secretEncrypted" = convert_to("secretHash", 'UTF8') 29WHERE "secretEncrypted" IS NULL 30 AND "secretHash" IS NOT NULL 31 AND "secretHash" !~ '^[0-9a-f]{64}$'; 32 33CREATE INDEX "webhook_deliveries_webhookId_createdAt_idx" 34 ON "webhook_deliveries"("webhookId", "createdAt"); 35