-- 005_usage_log.sql -- -- Per-request usage log for the plugin server-side endpoints. Privacy-first: -- * No raw IPs stored. IP is hashed with sha256(ip + account_id) so the same -- wallet/user across requests is linkable for debugging, but cross-account -- correlation is impossible and the raw IP cannot be recovered. -- * No user-agent string stored. Only categorized fields: ua_class (cli/sdk/ -- browser/mcp/unknown) and ua_os (macos/linux/windows/unknown). No version -- fingerprint. -- * No request bodies. The bytes_proposed / bytes_current fields capture -- cap-gate context only (sizes, not content). -- * Coarse outcome categories: ok / cap_exceeded / rate_limited / auth_failed -- / token_expired / not_found / dependency_failed. Surfaces what went wrong -- without leaking specifics. -- -- Retention: 30 days. Pruned daily by an admin cron or manual DELETE. -- -- Burden: 1 INSERT per /access /check-write /heartbeat /bind /email-bind call. -- Single small row (~150 bytes); indexed on the queries the dashboard needs. BEGIN; CREATE TABLE IF NOT EXISTS sibyl_plugin.usage_log ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), account_id UUID REFERENCES sibyl_plugin.accounts(account_id) ON DELETE CASCADE, ts TIMESTAMPTZ NOT NULL DEFAULT NOW(), endpoint TEXT NOT NULL, -- '/access', '/check-write', '/heartbeat', '/bind', '/email-bind' method TEXT NOT NULL, -- 'GET' or 'POST' status INT NOT NULL, -- HTTP status code returned duration_ms INT, -- server-side processing time ip_hash TEXT, -- sha256(raw_ip + account_id), 64 hex chars; null if no account context ua_class TEXT, -- 'cli' / 'sdk' / 'mcp' / 'browser' / 'unknown' ua_os TEXT, -- 'macos' / 'linux' / 'windows' / 'unknown' bytes_proposed INT, -- check-write only: proposed_delta_bytes bytes_current INT, -- check-write only: current_size_bytes outcome TEXT, -- 'ok' / 'cap_exceeded' / 'auth_failed' / etc. CONSTRAINT usage_log_endpoint_len CHECK (length(endpoint) <= 100), CONSTRAINT usage_log_method_len CHECK (length(method) <= 16), CONSTRAINT usage_log_ua_class_len CHECK (ua_class IS NULL OR length(ua_class) <= 32), CONSTRAINT usage_log_ua_os_len CHECK (ua_os IS NULL OR length(ua_os) <= 32), CONSTRAINT usage_log_outcome_len CHECK (outcome IS NULL OR length(outcome) <= 64), CONSTRAINT usage_log_iphash_len CHECK (ip_hash IS NULL OR length(ip_hash) = 64) ); -- Hot paths the dashboard queries: -- 1. Per-account rollup (drill-down) CREATE INDEX IF NOT EXISTS usage_log_account_ts_idx ON sibyl_plugin.usage_log (account_id, ts DESC); -- 2. Time-window aggregates (overview) CREATE INDEX IF NOT EXISTS usage_log_ts_idx ON sibyl_plugin.usage_log (ts DESC); -- 3. Endpoint distribution (overview) CREATE INDEX IF NOT EXISTS usage_log_endpoint_ts_idx ON sibyl_plugin.usage_log (endpoint, ts DESC); -- Schema version bump INSERT INTO sibyl_plugin.schema_version (version, applied_at, notes) VALUES ( 6, NOW(), 'usage_log: per-request privacy-first server-side log. Hashed IP, categorized UA, coarse outcome. 30d retention.' ) ON CONFLICT (version) DO NOTHING; COMMIT;