fix: red-team round 3 hardening across desktop, mobile, core and server

Batch of red-team r3 fixes that were in the working tree before the
2026-09-28 design overhaul, committed as one unit with their tests.

- desktop main: STT timeouts and sidecar, voice recording store, sync
  (credentials, audio, knowledge reindex, push gates), runtime
  provisioner, update policy, AltGr keybindings, voice-command policy,
  dictionary file codec/limits, meeting transcript condensing and a
  local recording ledger so interrupted-session recovery only closes
  meetings this device recorded (a phone's live meeting is left alone).
- mobile: login CSRF via implicit token callbacks rejected, account
  deletion/retention, durable queue retention, knowledge realtime
  without unfiltered DELETE, meeting re-record failure paths, cloud STT
  client, preferences store/resync.
- core: text chunking splits long unbroken transcripts to fit, template
  field policy, dictionary limits, meeting markdown inline handling.
- server: payple webhook policy and cancellation order scope, meeting
  document generation quota, team RPC null-role guard, unified LLM
  quota in-flight accounting, knowledge chunk vector index, meeting
  re-record failure paths (migrations 20260929*).
- ci: portable/runtime feed gates, update-policy schema, Forgejo file
  delete and alias planning.

Four older tests are updated to the new contracts rather than the old
behavior: token-pair auth callbacks are rejected, knowledge realtime no
longer subscribes to DELETE, long transcript lines are split, and
meeting recovery requires the local recording ledger for empty rows.
This commit is contained in:
Yun Chan 2026-09-28 20:45:52 +09:00
parent 2428ede03d
commit ba9ef9741e
161 changed files with 17056 additions and 2379 deletions

View file

@ -0,0 +1,341 @@
\set ON_ERROR_STOP on
-- A re-record that fails after upload restores the meeting it re-records
-- (migration 20260929040000). Local only: psql against the local Supabase
-- stack. Runs in a transaction and rolls back.
BEGIN;
CREATE OR REPLACE FUNCTION pg_temp.assert_true(condition boolean, message text)
RETURNS void
LANGUAGE plpgsql
AS $$
BEGIN
IF condition IS NOT TRUE THEN
RAISE EXCEPTION 'assertion_failed: %', message;
END IF;
END;
$$;
INSERT INTO auth.users (
id, aud, role, email, encrypted_password, email_confirmed_at,
raw_app_meta_data, raw_user_meta_data, created_at, updated_at
) VALUES (
'91000000-0000-4000-8000-000000000001', 'authenticated', 'authenticated',
'rerecord-failure-owner@example.invalid', crypt('fixture-password', gen_salt('bf')), now(),
'{"provider":"email","providers":["email"]}'::jsonb, '{}'::jsonb, now(), now()
);
-- M1: completed by a mobile capture (A1, job J1).
-- M2: first recording, no content yet.
-- M3: content from elsewhere (desktop): transcript rows, no mobile job, no audio row.
INSERT INTO public.meetings (
id, user_id, title, status, raw_transcript, minutes_markdown,
duration_ms, audio_storage_key
) VALUES
(
'92000000-0000-4000-8000-000000000001',
'91000000-0000-4000-8000-000000000001',
'Completed meeting', 'completed', 'old transcript', 'old minutes',
1000, '91000000-0000-4000-8000-000000000001/imports/a1.wav'
),
(
'92000000-0000-4000-8000-000000000002',
'91000000-0000-4000-8000-000000000001',
'First recording', 'recording', NULL, NULL, NULL, NULL
),
(
'92000000-0000-4000-8000-000000000003',
'91000000-0000-4000-8000-000000000001',
'Desktop meeting', 'completed', 'desktop transcript', NULL,
5000, 'desktop/original.wav'
);
INSERT INTO public.transcripts (meeting_id, segment_index, timestamp_ms, duration_ms, text)
VALUES
('92000000-0000-4000-8000-000000000001', 0, 0, 1000, 'old transcript'),
('92000000-0000-4000-8000-000000000003', 0, 0, 2000, 'desktop segment 0'),
('92000000-0000-4000-8000-000000000003', 1, 2000, 3000, 'desktop segment 1');
INSERT INTO public.audio_files (
id, user_id, meeting_id, source, original_name, storage_key, mime_type,
size_bytes, duration_ms, sha256, upload_status
) VALUES
(
'93000000-0000-4000-8000-000000000001',
'91000000-0000-4000-8000-000000000001',
'92000000-0000-4000-8000-000000000001',
'recording', 'a1.wav',
'91000000-0000-4000-8000-000000000001/imports/a1.wav',
'audio/wav', 32044, 1000, repeat('1', 64), 'uploaded'
),
(
'93000000-0000-4000-8000-000000000002',
'91000000-0000-4000-8000-000000000001',
'92000000-0000-4000-8000-000000000001',
'recording', 'a2.wav',
'91000000-0000-4000-8000-000000000001/imports/a2.wav',
'audio/wav', 64044, 2000, repeat('2', 64), 'uploaded'
),
(
'93000000-0000-4000-8000-000000000003',
'91000000-0000-4000-8000-000000000001',
'92000000-0000-4000-8000-000000000001',
'recording', 'a3.wav',
'91000000-0000-4000-8000-000000000001/imports/a3.wav',
'audio/wav', 96044, 3000, repeat('3', 64), 'uploaded'
),
(
'93000000-0000-4000-8000-000000000004',
'91000000-0000-4000-8000-000000000001',
'92000000-0000-4000-8000-000000000002',
'recording', 'first.wav',
'91000000-0000-4000-8000-000000000001/imports/first.wav',
'audio/wav', 32044, 1000, repeat('4', 64), 'uploaded'
),
(
'93000000-0000-4000-8000-000000000005',
'91000000-0000-4000-8000-000000000001',
'92000000-0000-4000-8000-000000000003',
'recording', 'desktop-rerecord.wav',
'91000000-0000-4000-8000-000000000001/imports/desktop-rerecord.wav',
'audio/wav', 32044, 9000, repeat('5', 64), 'failed'
);
INSERT INTO public.processing_jobs (
user_id, audio_file_id, meeting_id, kind, status, progress, attempt_count,
idempotency_key, result, started_at, completed_at
) VALUES (
'91000000-0000-4000-8000-000000000001',
'93000000-0000-4000-8000-000000000001',
'92000000-0000-4000-8000-000000000001',
'transcription', 'succeeded', 100, 1,
'mobile-meeting:m1:' || repeat('1', 64),
jsonb_build_object('audio_file_id', '93000000-0000-4000-8000-000000000001', 'duration_ms', 1000),
now() - interval '1 day', now() - interval '1 day'
);
SET LOCAL ROLE authenticated;
SELECT set_config(
'request.jwt.claims',
'{"sub":"91000000-0000-4000-8000-000000000001","role":"authenticated"}',
true
);
-- 1) Re-record M1 with A2; STT fails terminally after upload.
SELECT public.mobile_begin_meeting_recording('92000000-0000-4000-8000-000000000001');
SELECT public.mobile_queue_meeting_recording('92000000-0000-4000-8000-000000000001', 2000);
SELECT public.mobile_begin_meeting_processing(
'92000000-0000-4000-8000-000000000001',
'93000000-0000-4000-8000-000000000002',
'mobile-meeting:m1:' || repeat('2', 64)
);
SELECT pg_temp.assert_true(
(SELECT audio_storage_key FROM public.meetings
WHERE id = '92000000-0000-4000-8000-000000000001')
= '91000000-0000-4000-8000-000000000001/imports/a2.wav',
'precondition: processing points the meeting at the new capture'
);
SELECT public.mobile_mark_meeting_processing_failure(
'92000000-0000-4000-8000-000000000001',
'mobile-meeting:m1:' || repeat('2', 64),
'transcription', 'STT failed', true
);
SELECT pg_temp.assert_true(
EXISTS (
SELECT 1 FROM public.meetings
WHERE id = '92000000-0000-4000-8000-000000000001'
AND status = 'completed'
AND error_message IS NULL
AND raw_transcript = 'old transcript'
AND minutes_markdown = 'old minutes'
AND duration_ms = 1000
AND audio_storage_key = '91000000-0000-4000-8000-000000000001/imports/a1.wav'
),
'terminal failure of a re-record restores the completed meeting and its audio'
);
SELECT pg_temp.assert_true(
EXISTS (
SELECT 1 FROM public.processing_jobs
WHERE idempotency_key = 'mobile-meeting:m1:' || repeat('2', 64)
AND status = 'failed' AND error_message = 'STT failed'
),
'the failed job still records the failure'
);
RESET ROLE;
SELECT pg_temp.assert_true(
EXISTS (
SELECT 1 FROM public.audio_files
WHERE id = '93000000-0000-4000-8000-000000000002'
AND meeting_id IS NULL AND upload_status = 'deleted'
),
'the failed capture audio is detached'
);
SELECT pg_temp.assert_true(
EXISTS (
SELECT 1 FROM public.audio_purge_queue
WHERE user_id = '91000000-0000-4000-8000-000000000001'
AND storage_key = '91000000-0000-4000-8000-000000000001/imports/a2.wav'
),
'the failed capture audio is queued for purge'
);
SELECT pg_temp.assert_true(
EXISTS (
SELECT 1 FROM public.audio_files
WHERE id = '93000000-0000-4000-8000-000000000001'
AND meeting_id = '92000000-0000-4000-8000-000000000001'
AND upload_status = 'uploaded'
),
'the audio behind the kept transcript stays linked'
);
SELECT pg_temp.assert_true(
(SELECT meeting_id FROM public.audio_files
WHERE id = '93000000-0000-4000-8000-000000000003')
= '92000000-0000-4000-8000-000000000001',
'uploaded audio no failed job owns is not detached by an unrelated failure'
);
-- 2) Re-record M1 with A3; the user discards the queued item.
SET LOCAL ROLE authenticated;
SELECT set_config(
'request.jwt.claims',
'{"sub":"91000000-0000-4000-8000-000000000001","role":"authenticated"}',
true
);
SELECT public.mobile_begin_meeting_recording('92000000-0000-4000-8000-000000000001');
SELECT public.mobile_queue_meeting_recording('92000000-0000-4000-8000-000000000001', 3000);
SELECT public.mobile_begin_meeting_processing(
'92000000-0000-4000-8000-000000000001',
'93000000-0000-4000-8000-000000000003',
'mobile-meeting:m1:' || repeat('3', 64)
);
SELECT pg_temp.assert_true(
(SELECT status FROM public.mobile_fail_meeting_recording(
'92000000-0000-4000-8000-000000000001', 'Queued audio processing was cancelled'
)) = 'completed',
'fail_recording reports the restored status'
);
SELECT pg_temp.assert_true(
EXISTS (
SELECT 1 FROM public.meetings
WHERE id = '92000000-0000-4000-8000-000000000001'
AND status = 'completed'
AND error_message IS NULL
AND duration_ms = 1000
AND audio_storage_key = '91000000-0000-4000-8000-000000000001/imports/a1.wav'
),
'a discarded re-record restores the completed meeting and its audio'
);
RESET ROLE;
SELECT pg_temp.assert_true(
(SELECT meeting_id FROM public.audio_files
WHERE id = '93000000-0000-4000-8000-000000000003') IS NULL,
'the discarded capture audio is detached'
);
-- 3) A first recording keeps the error outcome and its audio for retry.
SET LOCAL ROLE authenticated;
SELECT set_config(
'request.jwt.claims',
'{"sub":"91000000-0000-4000-8000-000000000001","role":"authenticated"}',
true
);
SELECT public.mobile_queue_meeting_recording('92000000-0000-4000-8000-000000000002', 1000);
SELECT public.mobile_begin_meeting_processing(
'92000000-0000-4000-8000-000000000002',
'93000000-0000-4000-8000-000000000004',
'mobile-meeting:m2:' || repeat('4', 64)
);
SELECT public.mobile_mark_meeting_processing_failure(
'92000000-0000-4000-8000-000000000002',
'mobile-meeting:m2:' || repeat('4', 64),
'transcription', 'STT failed', true
);
SELECT pg_temp.assert_true(
EXISTS (
SELECT 1 FROM public.meetings
WHERE id = '92000000-0000-4000-8000-000000000002'
AND status = 'error'
AND error_message = 'STT failed'
),
'terminal failure of a first recording still reports an error'
);
SELECT pg_temp.assert_true(
(SELECT status FROM public.mobile_fail_meeting_recording(
'92000000-0000-4000-8000-000000000002', 'Recording failed'
)) = 'error',
'fail_recording of a first recording still reports an error'
);
RESET ROLE;
SELECT pg_temp.assert_true(
EXISTS (
SELECT 1 FROM public.audio_files
WHERE id = '93000000-0000-4000-8000-000000000004'
AND meeting_id = '92000000-0000-4000-8000-000000000002'
AND upload_status = 'uploaded'
),
'a failed first recording keeps its audio linked for retry'
);
-- 4) Content from elsewhere: a re-record whose upload failed (no job) keeps the
-- original playback key and restores the duration from the transcript span.
SET LOCAL ROLE authenticated;
SELECT set_config(
'request.jwt.claims',
'{"sub":"91000000-0000-4000-8000-000000000001","role":"authenticated"}',
true
);
SELECT public.mobile_begin_meeting_recording('92000000-0000-4000-8000-000000000003');
SELECT public.mobile_queue_meeting_recording('92000000-0000-4000-8000-000000000003', 9000);
SELECT public.mobile_fail_meeting_recording(
'92000000-0000-4000-8000-000000000003', 'Audio upload failed'
);
SELECT pg_temp.assert_true(
EXISTS (
SELECT 1 FROM public.meetings
WHERE id = '92000000-0000-4000-8000-000000000003'
AND status = 'completed'
AND error_message IS NULL
AND raw_transcript = 'desktop transcript'
AND duration_ms = 5000
AND audio_storage_key = 'desktop/original.wav'
),
'a failed re-record of foreign content keeps its key and transcript duration'
);
RESET ROLE;
SELECT pg_temp.assert_true(
(SELECT meeting_id FROM public.audio_files
WHERE id = '93000000-0000-4000-8000-000000000005') IS NULL,
'the failed upload row of the re-record is detached'
);
-- 5) Cancel before processing keeps the 20260929002700 behavior.
SET LOCAL ROLE authenticated;
SELECT set_config(
'request.jwt.claims',
'{"sub":"91000000-0000-4000-8000-000000000001","role":"authenticated"}',
true
);
SELECT public.mobile_begin_meeting_recording('92000000-0000-4000-8000-000000000001');
SELECT pg_temp.assert_true(
(SELECT status FROM public.mobile_cancel_meeting_recording(
'92000000-0000-4000-8000-000000000001'
)) = 'completed',
'cancel of a re-record still returns to completed'
);
-- 6) The helpers are not reachable through the API roles.
RESET ROLE;
SELECT pg_temp.assert_true(
NOT has_function_privilege('authenticated', 'public.mobile_meeting_has_content_v1(uuid)', 'EXECUTE')
AND NOT has_function_privilege('anon', 'public.mobile_meeting_has_content_v1(uuid)', 'EXECUTE')
AND NOT has_function_privilege(
'authenticated', 'public.mobile_restore_meeting_after_failed_capture_v1(uuid, uuid, uuid)', 'EXECUTE'
)
AND NOT has_function_privilege(
'anon', 'public.mobile_restore_meeting_after_failed_capture_v1(uuid, uuid, uuid)', 'EXECUTE'
),
'restore helpers are internal'
);
ROLLBACK;