feat(release): prepare 1.1.0 candidate
This commit is contained in:
parent
5a34f66981
commit
5205dcdfa9
736 changed files with 115667 additions and 12203 deletions
|
|
@ -0,0 +1,180 @@
|
|||
-- Extend the authenticated AI-output report boundary to generated meeting
|
||||
-- documents. Existing ephemeral generations continue to require a live receipt;
|
||||
-- meeting documents instead require an owned document plus its immutable
|
||||
-- generation audit row.
|
||||
|
||||
BEGIN;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.submit_content_report_v1(
|
||||
p_actor_id uuid,
|
||||
p_idempotency_key uuid,
|
||||
p_kind text,
|
||||
p_source_type text,
|
||||
p_source_id uuid,
|
||||
p_reason text,
|
||||
p_comment text,
|
||||
p_snapshot text
|
||||
) RETURNS jsonb
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = pg_catalog, public, auth, extensions
|
||||
AS $$
|
||||
DECLARE
|
||||
request_payload jsonb;
|
||||
request_digest text;
|
||||
existing_report public.content_reports%ROWTYPE;
|
||||
receipt public.content_generation_receipts%ROWTYPE;
|
||||
created_report public.content_reports%ROWTYPE;
|
||||
resolved_generation_receipt_id uuid;
|
||||
normalized_comment text;
|
||||
normalized_snapshot text;
|
||||
BEGIN
|
||||
IF auth.role() <> 'service_role' THEN
|
||||
RAISE EXCEPTION 'service_role_required' USING ERRCODE = '42501';
|
||||
END IF;
|
||||
IF p_actor_id IS NULL OR p_idempotency_key IS NULL OR p_source_id IS NULL THEN
|
||||
RAISE EXCEPTION 'content_report_identifiers_required' USING ERRCODE = '22023';
|
||||
END IF;
|
||||
IF p_kind <> 'ai_output'
|
||||
OR p_source_type NOT IN (
|
||||
'talk_response', 'command_response', 'action_response', 'meeting_document'
|
||||
) THEN
|
||||
RAISE EXCEPTION 'unsupported_content_report_source' USING ERRCODE = '22023';
|
||||
END IF;
|
||||
IF p_reason NOT IN (
|
||||
'harmful', 'sexual', 'hateful', 'violent', 'self_harm',
|
||||
'misinformation', 'privacy', 'spam', 'other'
|
||||
) THEN
|
||||
RAISE EXCEPTION 'invalid_content_report_reason' USING ERRCODE = '22023';
|
||||
END IF;
|
||||
|
||||
normalized_comment := CASE WHEN p_comment IS NULL THEN NULL ELSE trim(p_comment) END;
|
||||
normalized_snapshot := trim(coalesce(p_snapshot, ''));
|
||||
IF normalized_comment IS NOT NULL
|
||||
AND char_length(normalized_comment) NOT BETWEEN 1 AND 500 THEN
|
||||
RAISE EXCEPTION 'invalid_content_report_comment' USING ERRCODE = '22023';
|
||||
END IF;
|
||||
IF char_length(normalized_snapshot) NOT BETWEEN 1 AND 4000 THEN
|
||||
RAISE EXCEPTION 'invalid_content_report_snapshot' USING ERRCODE = '22023';
|
||||
END IF;
|
||||
|
||||
request_payload := jsonb_build_object(
|
||||
'kind', p_kind,
|
||||
'source', jsonb_build_object('type', p_source_type, 'generationId', p_source_id),
|
||||
'reason', p_reason,
|
||||
'comment', normalized_comment,
|
||||
'snapshot', normalized_snapshot
|
||||
);
|
||||
request_digest := encode(extensions.digest(request_payload::text, 'sha256'), 'hex');
|
||||
|
||||
-- One actor lock covers idempotency, duplicate-source detection and both
|
||||
-- rate windows. Concurrent calls cannot observe the same remaining slot.
|
||||
PERFORM pg_advisory_xact_lock(
|
||||
pg_catalog.hashtextextended('d3ro:content-report:' || p_actor_id::text, 0)
|
||||
);
|
||||
|
||||
SELECT * INTO existing_report
|
||||
FROM public.content_reports
|
||||
WHERE reporter_id = p_actor_id AND idempotency_key = p_idempotency_key;
|
||||
|
||||
IF existing_report.id IS NOT NULL THEN
|
||||
IF existing_report.request_hash <> request_digest THEN
|
||||
RAISE EXCEPTION 'content_report_idempotency_conflict' USING ERRCODE = 'PT409';
|
||||
END IF;
|
||||
RETURN jsonb_build_object(
|
||||
'reportId', existing_report.id,
|
||||
'status', 'submitted',
|
||||
'idempotent', true,
|
||||
'createdAt', existing_report.created_at
|
||||
);
|
||||
END IF;
|
||||
|
||||
-- Keep account deletion from invalidating the reporter foreign key between
|
||||
-- source verification and the report insert. A deletion that wins first is
|
||||
-- intentionally indistinguishable from any other missing source.
|
||||
PERFORM 1 FROM auth.users WHERE id = p_actor_id FOR KEY SHARE;
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'content_report_source_not_found' USING ERRCODE = 'P0002';
|
||||
END IF;
|
||||
|
||||
IF p_source_type = 'meeting_document' THEN
|
||||
-- A document is reportable only when it belongs to the actor and an
|
||||
-- immutable generation audit row proves that the document came from the
|
||||
-- server-side generation path. Manual or cross-user rows fail identically.
|
||||
PERFORM 1
|
||||
FROM public.meeting_documents AS document
|
||||
INNER JOIN public.meeting_document_generation_audit AS generation
|
||||
ON generation.document_id = document.id
|
||||
AND generation.user_id = document.user_id
|
||||
AND generation.meeting_id = document.meeting_id
|
||||
WHERE document.id = p_source_id
|
||||
AND document.user_id = p_actor_id
|
||||
FOR KEY SHARE OF document, generation;
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'content_report_source_not_found' USING ERRCODE = 'P0002';
|
||||
END IF;
|
||||
resolved_generation_receipt_id := NULL;
|
||||
ELSE
|
||||
SELECT * INTO receipt
|
||||
FROM public.content_generation_receipts
|
||||
WHERE id = p_source_id
|
||||
AND user_id = p_actor_id
|
||||
AND purpose = p_source_type
|
||||
AND expires_at > now()
|
||||
FOR KEY SHARE;
|
||||
IF receipt.id IS NULL THEN
|
||||
-- Missing, expired and cross-user receipts intentionally look identical.
|
||||
RAISE EXCEPTION 'content_report_source_not_found' USING ERRCODE = 'P0002';
|
||||
END IF;
|
||||
resolved_generation_receipt_id := receipt.id;
|
||||
END IF;
|
||||
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM public.content_reports
|
||||
WHERE reporter_id = p_actor_id
|
||||
AND source_type = p_source_type
|
||||
AND source_id = p_source_id
|
||||
) THEN
|
||||
RAISE EXCEPTION 'content_report_source_already_reported' USING ERRCODE = 'PT409';
|
||||
END IF;
|
||||
|
||||
IF (
|
||||
SELECT count(*) FROM public.content_reports
|
||||
WHERE reporter_id = p_actor_id
|
||||
AND created_at > now() - interval '1 hour'
|
||||
) >= 10 OR (
|
||||
SELECT count(*) FROM public.content_reports
|
||||
WHERE reporter_id = p_actor_id
|
||||
AND created_at > now() - interval '24 hours'
|
||||
) >= 30 THEN
|
||||
RAISE EXCEPTION 'content_report_rate_limited' USING ERRCODE = 'PT429';
|
||||
END IF;
|
||||
|
||||
INSERT INTO public.content_reports(
|
||||
reporter_id, idempotency_key, request_hash, kind, source_type, source_id,
|
||||
generation_receipt_id, reason, reporter_comment, reported_snapshot,
|
||||
snapshot_sha256
|
||||
) VALUES (
|
||||
p_actor_id, p_idempotency_key, request_digest, p_kind, p_source_type, p_source_id,
|
||||
resolved_generation_receipt_id, p_reason, normalized_comment, normalized_snapshot,
|
||||
encode(extensions.digest(normalized_snapshot, 'sha256'), 'hex')
|
||||
)
|
||||
RETURNING * INTO created_report;
|
||||
|
||||
RETURN jsonb_build_object(
|
||||
'reportId', created_report.id,
|
||||
'status', 'submitted',
|
||||
'idempotent', false,
|
||||
'createdAt', created_report.created_at
|
||||
);
|
||||
END;
|
||||
$$;
|
||||
|
||||
REVOKE ALL ON FUNCTION public.submit_content_report_v1(
|
||||
uuid, uuid, text, text, uuid, text, text, text
|
||||
) FROM PUBLIC, anon, authenticated;
|
||||
GRANT EXECUTE ON FUNCTION public.submit_content_report_v1(
|
||||
uuid, uuid, text, text, uuid, text, text, text
|
||||
) TO service_role;
|
||||
|
||||
COMMIT;
|
||||
Loading…
Add table
Add a link
Reference in a new issue