\c dcntrlized; -- 008_log_events.sql — durable, anonymised service log. -- -- User request (2026-10-18): the service should keep its operational log in the -- database, not only on stdout, and it must not keep raw IPs or user agents. -- Explicit columns end in `_hash` so a future reader cannot insert a raw value -- by accident, and there is deliberately NO column for a token, request body or -- query string: those must never be persisted (in Alarm they carry admin and -- subscribe tokens). -- -- Identifier hashing is NOT the analytics scheme: -- * alert_hash / user_hash: SHA-256 hex of the UUID (analytics scheme; UUIDs -- carry ~122 random bits, so a plain digest is one-way). -- * ip_hash / ua_hash: Argon2id with a service-generated public salt and a -- tuned work factor (internal/loghash). IPs are a 2^32 space, so a plain -- digest is a lookup table away from being reversed; only the KDF's cost -- protects them. The two schemes are intentionally different — do not -- "unify" them, and do not lower the Argon2id parameters to make the logger -- faster: that would silently un-anonymise every historical row. -- -- All columns are nullable except id/occurred_at/level/kind/message: rows are -- anonymised by construction, so this table can be exported for support without -- leaking who or where. Retention is enforced by the sweeper (age + row cap), -- see API.md; the table must never grow without bound. -- -- Idempotent (same guard pattern as 002–007). DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = 'alarm') THEN RETURN; END IF; CREATE TABLE IF NOT EXISTS alarm.log_events ( id BIGSERIAL PRIMARY KEY, occurred_at TIMESTAMPTZ NOT NULL DEFAULT now(), level TEXT NOT NULL, kind TEXT NOT NULL, message TEXT NOT NULL, ip_hash TEXT, ua_hash TEXT, alert_hash TEXT, user_hash TEXT, CONSTRAINT log_events_level_check CHECK (level IN ('debug', 'info', 'warn', 'error')) ); CREATE INDEX IF NOT EXISTS log_events_occurred_idx ON alarm.log_events (occurred_at); CREATE INDEX IF NOT EXISTS log_events_kind_idx ON alarm.log_events (kind, occurred_at); IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'alarm_user') THEN GRANT SELECT, INSERT, DELETE ON alarm.log_events TO alarm_user; GRANT USAGE, SELECT ON SEQUENCE alarm.log_events_id_seq TO alarm_user; END IF; END $$;