Files
MikrotikManager/backend/drizzle/0044_mcp_routing_manual_inverse_charges.sql
T

94 lines
5.9 KiB
PL/PgSQL

-- Additive durable scoped manual inverse-one charge. No execution grant.
-- Existing 0043 is immutable. Logical charge identities outlive family FKs.
BEGIN;
DO $$ BEGIN
IF to_regclass('public.mcp_routing_manual_inverse_charges') IS NOT NULL
THEN RAISE EXCEPTION 'unmarked manual inverse storage requires reconciliation'; END IF;
END $$;
CREATE TABLE public.mcp_routing_manual_inverse_charges (
recovery_operation_id uuid PRIMARY KEY,
recovery_plan_id uuid NOT NULL,
source_operation_id uuid NOT NULL,
source_plan_id uuid NOT NULL,
source_step_index integer NOT NULL CHECK(source_step_index=0),
profile_id uuid NOT NULL,
profile_version integer NOT NULL CHECK(profile_version>0),
connection_id uuid NOT NULL,
owner_id text NOT NULL CHECK(octet_length(owner_id) BETWEEN 1 AND 1000),
charge_units integer NOT NULL CHECK(charge_units=1),
binding_sealed text NOT NULL CHECK(octet_length(binding_sealed) BETWEEN 1 AND 262144),
binding_proof text NOT NULL CHECK(binding_proof ~ '^[a-f0-9]{64}$'),
created_at timestamptz NOT NULL,
CONSTRAINT mcp_manual_inverse_source_step_unique UNIQUE(source_operation_id,source_step_index)
);
CREATE INDEX mcp_routing_manual_inverse_charges_window
ON public.mcp_routing_manual_inverse_charges(profile_id,created_at);
CREATE INDEX mcp_routing_manual_inverse_charges_retention
ON public.mcp_routing_manual_inverse_charges(created_at,recovery_operation_id);
CREATE INDEX mcp_scoped_recovery_manual_history
ON public.mcp_scoped_recovery_intents(mode,source_operation_id,source_step_index)
WHERE mode='manual';
CREATE FUNCTION public.mcp_routing_manual_inverse_charge_source() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
IF NOT EXISTS(
SELECT 1 FROM public.mcp_operations o
JOIN public.mcp_plans p ON p.id=o.plan_id AND p.connection_id=o.connection_id
JOIN public.mcp_routing_profiles r ON r.id=NEW.profile_id AND r.connection_id=o.connection_id
JOIN public.mcp_operations source ON source.id=NEW.source_operation_id AND source.connection_id=o.connection_id
JOIN public.mcp_plans sp ON sp.id=source.plan_id AND sp.connection_id=source.connection_id
JOIN public.mcp_connections c ON c.id=o.connection_id
WHERE o.id=NEW.recovery_operation_id AND p.id=NEW.recovery_plan_id
AND o.connection_id=NEW.connection_id AND c.owner_id=NEW.owner_id
AND r.version=NEW.profile_version AND o.state='pending' AND p.state='executing'
AND p.operation_id='operations.recover' AND p.operation_version=7
AND sp.id=NEW.source_plan_id AND sp.operation_id='routing.community.switch' AND sp.operation_version=7
AND source.state IN('applied','partial')
) THEN RAISE EXCEPTION 'manual inverse charge differs from pending admission'; END IF;
RETURN NEW;
END $$;
CREATE TRIGGER mcp_routing_manual_inverse_charge_source BEFORE INSERT ON public.mcp_routing_manual_inverse_charges
FOR EACH ROW EXECUTE FUNCTION public.mcp_routing_manual_inverse_charge_source();
CREATE FUNCTION public.mcp_routing_manual_inverse_charge_immutable() RETURNS trigger LANGUAGE plpgsql AS $$
DECLARE retention integer;
BEGIN
IF TG_OP<>'DELETE' THEN RAISE EXCEPTION 'immutable manual inverse charge; rewrite/refund forbidden'; END IF;
SELECT requests_retention_days INTO retention FROM public.mcp_settings WHERE id=1 FOR SHARE;
IF retention IS NULL OR retention NOT BETWEEN 1 AND 365
OR OLD.created_at>clock_timestamp()-interval '1 hour'
OR OLD.created_at>=clock_timestamp()-make_interval(days=>retention)
OR EXISTS(SELECT 1 FROM public.mcp_operations WHERE id=OLD.recovery_operation_id AND state IN('pending','running'))
THEN RAISE EXCEPTION 'manual inverse charge still retained or unresolved'; END IF;
RETURN OLD;
END $$;
CREATE TRIGGER mcp_routing_manual_inverse_charge_immutable BEFORE UPDATE OR DELETE ON public.mcp_routing_manual_inverse_charges
FOR EACH ROW EXECUTE FUNCTION public.mcp_routing_manual_inverse_charge_immutable();
CREATE TRIGGER mcp_routing_manual_inverse_charge_no_truncate BEFORE TRUNCATE ON public.mcp_routing_manual_inverse_charges
FOR EACH STATEMENT EXECUTE FUNCTION public.mcp_routing_manual_inverse_charge_immutable();
-- This is a version/identity fence, never authentication of encrypted source.
-- Old binaries cannot create a fresh v6 manual intent after this DDL commits.
-- Already-claimed old work requires the normal drained-writer cutover.
CREATE FUNCTION public.mcp_scoped_recovery_manual_version() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
IF NEW.mode='manual' AND NOT pg_try_advisory_xact_lock(hashtextextended('mcp-runner-admission',0))
THEN RAISE EXCEPTION 'manual intent admission snapshot busy'; END IF;
IF NEW.mode='manual' AND NOT EXISTS(
SELECT 1 FROM public.mcp_operations o
JOIN public.mcp_plans p ON p.id=o.plan_id AND p.connection_id=o.connection_id
JOIN public.mcp_operation_steps s ON s.operation_id=o.id AND s.step_index=0
JOIN public.mcp_operations source ON source.id=NEW.source_operation_id AND source.connection_id=o.connection_id
WHERE o.id=NEW.recovery_operation_id AND o.state='running'
AND p.state='executing' AND p.operation_id='operations.recover' AND p.operation_version=7
AND NEW.source_step_index=0 AND s.state='intent'
AND s.intended_state->>'kind'='service'
AND s.intended_state->>'service'='routing.community.scoped.recover'
AND s.intended_state->'input'->>'mode'='manual'
AND s.intended_state->'input'->>'sourceOperationId'=NEW.source_operation_id::text
AND s.intended_state->'input'->>'receiptId'=NEW.recovery_receipt_id::text
) THEN RAISE EXCEPTION 'manual inverse requires current exact running recovery'; END IF;
RETURN NEW;
END $$;
CREATE TRIGGER mcp_scoped_recovery_manual_version BEFORE INSERT ON public.mcp_scoped_recovery_intents
FOR EACH ROW EXECUTE FUNCTION public.mcp_scoped_recovery_manual_version();
INSERT INTO public.schema_migrations(id) VALUES('0044_mcp_routing_manual_inverse_charges') ON CONFLICT(id) DO NOTHING;
COMMIT;