-- Sense Outbox relay fencing and Bell global audit facts. ALTER TABLE sense.device_operation_outbox ADD COLUMN IF NOT EXISTS relay_lease_owner text, ADD COLUMN IF NOT EXISTS relay_lease_token bigint NOT NULL DEFAULT 0, ADD COLUMN IF NOT EXISTS relay_lease_until timestamptz, ADD COLUMN IF NOT EXISTS last_error_code text, ADD COLUMN IF NOT EXISTS dead_lettered_at timestamptz; ALTER TABLE sense.device_operation_outbox DROP CONSTRAINT IF EXISTS sense_outbox_relay_lease_pair; ALTER TABLE sense.device_operation_outbox ADD CONSTRAINT sense_outbox_relay_lease_pair CHECK ( (relay_lease_owner IS NULL AND relay_lease_until IS NULL) OR (relay_lease_owner IS NOT NULL AND btrim(relay_lease_owner) <> '' AND relay_lease_until IS NOT NULL) ); ALTER TABLE sense.device_operation_outbox DROP CONSTRAINT IF EXISTS sense_outbox_relay_token_nonnegative; ALTER TABLE sense.device_operation_outbox ADD CONSTRAINT sense_outbox_relay_token_nonnegative CHECK (relay_lease_token >= 0); ALTER TABLE sense.device_operation_outbox DROP CONSTRAINT IF EXISTS sense_outbox_relay_error_length; ALTER TABLE sense.device_operation_outbox ADD CONSTRAINT sense_outbox_relay_error_length CHECK ( last_error_code IS NULL OR ( char_length(last_error_code) BETWEEN 1 AND 64 AND last_error_code ~ '^[a-z][a-z0-9_]*$' ) ); ALTER TABLE sense.device_operation_outbox DROP CONSTRAINT IF EXISTS sense_outbox_relay_terminal_state; ALTER TABLE sense.device_operation_outbox ADD CONSTRAINT sense_outbox_relay_terminal_state CHECK ( delivered_at IS NULL OR dead_lettered_at IS NULL ); CREATE INDEX IF NOT EXISTS sense_outbox_relay_due_idx ON sense.device_operation_outbox( COALESCE(next_attempt_at, available_at), event_id ) WHERE delivered_at IS NULL AND dead_lettered_at IS NULL; CREATE TABLE IF NOT EXISTS bell.audit_events ( source_system text NOT NULL, event_id text NOT NULL, schema_version smallint NOT NULL, event_type text NOT NULL, tenant_id text NOT NULL, site_id text NOT NULL, device_id text NOT NULL, actor_type text NOT NULL, actor_id text NOT NULL, reason text, trace_id text, aggregate_generation bigint NOT NULL, quota_source_version bigint, area_policy_source_version bigint, payload jsonb NOT NULL, occurred_at timestamptz NOT NULL, received_at timestamptz NOT NULL DEFAULT clock_timestamp(), record_hash bytea NOT NULL, PRIMARY KEY (source_system, event_id), CONSTRAINT bell_audit_source_system CHECK (source_system = 'sense'), CONSTRAINT bell_audit_event_id CHECK (event_id ~ '^audit_[0-9a-f]{32}$'), CONSTRAINT bell_audit_schema_version CHECK (schema_version IN (1, 2)), CONSTRAINT bell_audit_event_type CHECK (event_type IN ( 'device.created', 'device.desired_state.accepted', 'device.configuration.accepted' )), CONSTRAINT bell_audit_identity_not_blank CHECK ( btrim(tenant_id) <> '' AND btrim(site_id) <> '' AND btrim(device_id) <> '' AND btrim(actor_id) <> '' ), CONSTRAINT bell_audit_actor_type CHECK (actor_type IN ('user', 'service', 'system')), CONSTRAINT bell_audit_generation_positive CHECK (aggregate_generation >= 1), CONSTRAINT bell_audit_projection_versions CHECK ( (quota_source_version IS NULL OR quota_source_version >= 1) AND (area_policy_source_version IS NULL OR area_policy_source_version >= 1) ), CONSTRAINT bell_audit_reason_length CHECK (reason IS NULL OR char_length(reason) <= 500), CONSTRAINT bell_audit_trace_length CHECK (trace_id IS NULL OR char_length(trace_id) <= 128), CONSTRAINT bell_audit_payload_object CHECK (jsonb_typeof(payload) = 'object'), CONSTRAINT bell_audit_record_hash_length CHECK (octet_length(record_hash) = 32) ); ALTER TABLE bell.audit_events OWNER TO bell_app; DROP TRIGGER IF EXISTS bell_audit_events_immutable ON bell.audit_events; CREATE TRIGGER bell_audit_events_immutable BEFORE UPDATE OR DELETE ON bell.audit_events FOR EACH ROW EXECUTE FUNCTION bell.reject_immutable_change(); CREATE INDEX IF NOT EXISTS bell_audit_scope_time_idx ON bell.audit_events(tenant_id, site_id, occurred_at DESC, event_id DESC); CREATE INDEX IF NOT EXISTS bell_audit_device_time_idx ON bell.audit_events(tenant_id, site_id, device_id, occurred_at DESC, event_id DESC); CREATE TABLE IF NOT EXISTS bell.audit_relay_receipts ( key_id text NOT NULL, nonce text NOT NULL, request_hash bytea NOT NULL, response_status integer NOT NULL, response_body jsonb NOT NULL, received_at timestamptz NOT NULL DEFAULT clock_timestamp(), expires_at timestamptz NOT NULL, PRIMARY KEY (key_id, nonce), CONSTRAINT bell_audit_receipt_key_id CHECK ( char_length(key_id) BETWEEN 1 AND 64 AND key_id ~ '^[A-Za-z0-9][A-Za-z0-9._-]*$' ), CONSTRAINT bell_audit_receipt_nonce CHECK ( char_length(nonce) BETWEEN 22 AND 64 AND nonce ~ '^[A-Za-z0-9_-]+$' ), CONSTRAINT bell_audit_receipt_hash_length CHECK (octet_length(request_hash) = 32), CONSTRAINT bell_audit_receipt_status CHECK (response_status = 200), CONSTRAINT bell_audit_receipt_body CHECK (jsonb_typeof(response_body) = 'object'), CONSTRAINT bell_audit_receipt_ttl CHECK ( expires_at >= received_at + interval '10 minutes' AND expires_at <= received_at + interval '11 minutes' ) ); ALTER TABLE bell.audit_relay_receipts OWNER TO bell_app; CREATE INDEX IF NOT EXISTS bell_audit_receipt_expiry_idx ON bell.audit_relay_receipts(expires_at, key_id, nonce); INSERT INTO bell.schema_migrations(version) VALUES (4) ON CONFLICT (version) DO NOTHING; INSERT INTO sense.schema_migrations(version) VALUES (6) ON CONFLICT (version) DO NOTHING;