Skip to content

feat(meetings): encrypted meeting media, consent record, and reader gating (M2, #2576) - #2660

Open
JOY (JOY) wants to merge 2 commits into
mainfrom
antigravity/meetings-m2-2576
Open

JOY (JOY) wants to merge 2 commits into
mainfrom
antigravity/meetings-m2-2576

Conversation

@JOY

@JOY JOY (JOY) commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Closes #2576
Refs #932

Summary

This PR implements Milestone M2 for AI Meeting Notes: encrypted meeting media (audio and video), consent records, attendee reader gating, authenticated playback, and key shredding.

  • Database (crove_meetings):

    • Schema with 5 tables: meeting, meeting_reader, audio_segment, meeting_video, and media_key.
    • Restrictive RLS gating rows to attendees and explicitly granted meeting readers.
    • Pinned SECURITY DEFINER functions with fixed search_path: is_meeting_reader, media_key_seal, media_key_read, media_key_shred, media_key_reseal_due, media_key_reseal, media_key_counts, meeting_owner_list, and meeting_owner_delete.
    • Expands file.source check to accept meeting_audio and meeting_video.
    • Updates crove_kernel.org_purge and crove_kernel.health for meetings.
  • Cryptography and Key Management (@crove/kernel-runtime):

    • Chunked AES-256-GCM blob cipher (encryptBlobBuffer, decryptBlobBuffer) with 64 KB chunks, 96-bit random IVs, and authenticated AAD context.
    • Media key wrapping via crove.meetings.v1|wrap HKDF subkey.
    • Reseal wiring in apps/kernel/src/credential-reseal.ts to reseal media keys on credential rotation.
    • Key shredding in crove_meetings.media_key_shred upon meeting deletion, rendering ciphertexts permanently unrecoverable.
  • Module @crove/mod-meetings and Kernel Wiring:

    • Manifest registering tables, column data classes, and permissions.
    • Staged upload support with limits: 300 MB for meeting_audio, 2 GB for meeting_video.
    • Refusal of meeting files on generic /api/files/:id route (PURPOSE_DOWNLOAD_REFUSED).
    • Sensitive-audited authenticated playback routes /api/meetings/:id/audio and /api/meetings/:id/video with HTTP 206 partial content range support and SHA-256 integrity checks.
    • Gated under CROVE_MODULES (meetings is off by default).
  • Web UI (apps/app):

    • Cuoc hop sub-nav tab in Ludy Notes, visible only when meetings module is enabled.
    • Meeting list and creation modal with mandatory recording consent confirmation.
    • Meeting detail page with authenticated audio and video players, reader sharing management, and delete confirmation with key shredding.

Acceptance

User-Visible Changes

  • Users in organizations with the meetings module enabled will see a new Cuoc hop tab inside Ludy Notes (/o/:slug/meetings).
  • Users can create a meeting by uploading audio (up to 300 MB) or video (up to 2 GB), after explicitly confirming recording consent.
  • Attendees can listen to audio and watch video through an authenticated media player with streaming seek support.
  • Non-attendees cannot view, listen to, or access the meeting unless explicitly added as readers.
  • Meeting deletion immediately shreds the data encryption key in the database, ensuring immediate unrecoverability even if blob storage remains.
  • In organizations without the meetings module enabled, the feature is completely hidden and inaccessible.

Evidence Against Acceptance Criteria

  • Schema and RLS: 5 tables created in crove_meetings with restrictive RLS and pinned search_path. Tested in modules/meetings/test/meetings.test.ts.
  • Encryption: AES-256-GCM chunked blob cipher tested with integrity checks, invalid header rejection, and key wrapping in packages/kernel-runtime/test/blob-cipher.test.ts (9 tests passed).
  • Staged Upload and Generic Download Refusal: Staged uploads tested with purposes meeting_audio and meeting_video. Generic /api/files/:id refuses download with PURPOSE_DOWNLOAD_REFUSED in apps/kernel/test/http.test.ts (54 tests passed).
  • Authenticated Playback and Range: /api/meetings/:id/audio and /api/meetings/:id/video support HTTP 206 Range headers and verify SHA-256 checksums before returning decrypted data.
  • Key Shredding and Reseal: media_key_shred and media_key_reseal tested. Key rotation reseals unwrapped keys using active credential broker version.
  • Quality Gates:
    • pnpm lint: passed (0 warnings, 0 errors, no raw colors).
    • pnpm typecheck: passed across all 19 workspace projects.
    • pnpm knip: passed (0 unused dependencies).
    • pnpm depcruise: passed (0 dependency violations).
    • pnpm docs:check: passed (155 docs files OK).
    • node tools/ci/spdx-header.mjs origin/main: passed (13 new source files OK).
    • pnpm migration:rebase: verified clean ordering after origin/main.

Review Results

  • Codex CLI review: Completed with 8 recommendations (addressed media claiming, error translations, prober defaults, reseal schema, and header checks).
  • Fresh-context research subagent review: Completed with 5 findings (B1 reseal column definition, B2 frontend media claiming flow, B3 authenticated media URLs, B4 kernel error translation mappings, B5 prober default module array). All findings addressed and verified.

Risk Left

  • Meeting transcription pipeline (Milestone M3) will introduce background worker jobs (crove_jobs.job), which are not yet implemented in M2.

📌 TL;DR

This PR introduces the "Meetings" module, enabling users to record, upload, and manage encrypted audio/video meeting recordings with mandatory consent tracking. It implements a new sub-navigation system for the Notes/Meetings/My-Work cluster and integrates media key management into the kernel's credential resealing workflow.

🎯 Type of Change

  • 🚀 New feature
  • 🐛 Bugfix
  • 🧹 Refactor
  • ⚡ Performance
  • 📚 Documentation
  • ⚙️ CI / Configuration

🔍 Changes Walkthrough

File Summary of Changes
apps/app/src/components/notes-subnav.tsx New: Creates a reusable sub-navigation component for the "Notes," "My Work," and "Meetings" tabs, with conditional rendering for the Meetings tab based on module configuration.
apps/app/src/i18n/messages/en.ts Adds English translations for the new Meetings module, including UI labels, form fields, consent notices, and error messages.
apps/app/src/i18n/messages/vi.ts Adds Vietnamese translations for the new Meetings module, mirroring the English structure.
apps/app/src/lib/api.ts Extends the upload logic to support meeting_audio and meeting_video purposes with specific size limits. Adds a stageMediaUpload function to handle presigned uploads for large media files.
apps/app/src/lib/kernel-errors.ts Registers MEETING_NOT_FOUND as a known kernel error code.
apps/app/src/router.tsx Registers new routes for /o/$slug/meetings (list) and /o/$slug/meetings/$id (detail) under the organization route tree.
apps/app/src/routes/meeting-detail.tsx New: Implements the meeting detail view, including media playback (audio/video), consent record display, reader management, and a destructive "shred and delete" action.
apps/app/src/routes/meetings.tsx New: Implements the meetings list view with search functionality and a "New Meeting" modal that enforces a mandatory consent confirmation gate before allowing media upload.
apps/app/src/routes/my-work.tsx Integrates the new NotesSubnav component into the My Work page.
apps/app/src/routes/notes.tsx Integrates the new NotesSubnav component into the Notes page.
apps/app/test/kernel-errors.test.ts Updates tests to account for new meeting-related error codes that are not shown to users directly.
apps/kernel/package.json Adds @crove/mod-meetings as a workspace dependency.
apps/kernel/src/config.ts Adds meetings to the list of available modules.
apps/kernel/src/credential-reseal.ts Extends the credential resealing process to handle meeting media keys, ensuring encrypted media remains accessible after key rotation.
apps/kernel/src/http.ts Adds HTTP endpoints for meeting media playback (with decryption) and restricts direct file access for meeting media. Integrates the credential broker for key unwrapping.

📊 Architectural Flow

sequenceDiagram
    participant User
    participant App as Frontend (React)
    participant API as Kernel API
    participant Broker as Credential Broker
    participant Store as Blob Store

    User->>App: Click "Record New Meeting"
    App->>User: Display Consent Gate
    User->>App: Confirm Consent & Select Files
    App->>API: POST /api/files/uploads (purpose: meeting_audio)
    API-->>App: Return Presigned URL & Upload ID
    App->>Store: Upload Media File (Direct)
    App->>API: POST /api/meetings (Create Meeting)
    API->>Broker: Generate & Wrap Media Key
    API-->>App: Return Meeting ID
    App->>API: POST /api/meetings/:id/claim-media
    API->>Store: Verify & Link Media to Meeting

    Note over User,Store: Playback Flow
    User->>App: Click Play Audio
    App->>API: GET /api/meetings/:id/audio
    API->>Broker: Unwrap Media Key
    API->>Store: Fetch Encrypted Blob
    API->>API: Decrypt Blob
    API-->>App: Return Decrypted Audio Stream
Loading

…ating (M2, #2576)

- Create crove_meetings schema with 5 tables: meeting, meeting_reader, audio_segment, meeting_video, media_key
- Enforce reader-gated restrictive RLS and pinned SECURITY DEFINER functions
- Implement AES-256-GCM chunked blob cipher with HKDF wrap subkey in @crove/kernel-runtime
- Wire credential reseal rotation in apps/kernel/src/credential-reseal.ts
- Create @crove/mod-meetings module with manifest, commands, queries, and orpc endpoints
- Expand staged upload purposes: meeting_audio (300 MB) and meeting_video (2 GB)
- Implement authenticated media playback endpoints with HTTP 206 Range support and SHA-256 verification
- Add key shredding on meeting deletion
- Add Cuoc hop sub-tab under Ludy Notes with consent gate modal and player
- Update crove_kernel.org_purge and crove_kernel.health for meetings module
- Closes #2576, Refs #932
@vercel

vercel Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
ludy Ready Ready Preview Nov 6, 2026 2:00am UTC
ludy-app Ready Ready Preview Nov 6, 2026 2:00am UTC

Request Review

@coderabbitai

coderabbitai Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 21 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: b8530a55-de89-4430-98d6-5394ba73c9da
Commits

Reviewing files that changed from the base of the PR and between e7bb674 and 1b8ce1f.

Files ignored due to path filters: 2

  • packages/db/src/generated/database.ts is excluded by !**/generated/**
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
Files selected for processing: 52

  • apps/app/src/components/notes-subnav.tsx
  • apps/app/src/i18n/messages/en.ts
  • apps/app/src/i18n/messages/vi.ts
  • apps/app/src/lib/api.ts
  • apps/app/src/lib/kernel-errors.ts
  • apps/app/src/router.tsx
  • apps/app/src/routes/meeting-detail.tsx
  • apps/app/src/routes/meetings.tsx
  • apps/app/src/routes/my-work.tsx
  • apps/app/src/routes/notes.tsx
  • apps/app/test/kernel-errors.test.ts
  • apps/app/test/module-screens.test.ts
  • apps/kernel/package.json
  • apps/kernel/src/config.ts
  • apps/kernel/src/credential-reseal.ts
  • apps/kernel/src/http.ts
  • apps/kernel/src/kernel.ts
  • apps/kernel/src/rpc.ts
  • apps/kernel/test/http.test.ts
  • apps/kernel/test/invariant/fixtures/manifests.ts
  • apps/kernel/test/invariant/rls-matrix.db.test.ts
  • apps/kernel/test/jobs-schema.test.ts
  • apps/kernel/test/manifest-permissions.ts
  • apps/kernel/test/manifests.ts
  • apps/kernel/test/meetings.db.test.ts
  • apps/kernel/test/prober.test.ts
  • modules/meetings/package.json
  • modules/meetings/src/index.ts
  • modules/meetings/src/manifest.ts
  • modules/meetings/src/meetings.ts
  • modules/meetings/test/meetings.test.ts
  • modules/meetings/tsconfig.json
  • packages/db/src/schema.ts
  • packages/db/test/check-unions.db.test.ts
  • packages/db/test/schema.test.ts
  • packages/kernel-contract/src/contract.ts
  • packages/kernel-contract/src/index.ts
  • packages/kernel-contract/src/meetings.ts
  • packages/kernel-contract/src/screens.ts
  • packages/kernel-runtime/src/blob-cipher.ts
  • packages/kernel-runtime/src/index.ts
  • packages/kernel-runtime/src/storage.ts
  • packages/kernel-runtime/src/vault.ts
  • packages/kernel-runtime/test/blob-cipher.test.ts
  • packages/kernel/src/commands.ts
  • packages/kernel/src/files.ts
  • packages/kernel/src/queries.ts
  • supabase/migrations/20261123120000_crove_meetings_core.sql
  • supabase/migrations/20261123120200_crove_kernel_health_meetings.sql
  • supabase/migrations/20261123130000_crove_kernel_org_purge_meetings.sql
  • tools/db-types.mjs
  • tools/test/module-schemas.test.ts
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@dos dos Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⏱️ Code Review completed (50 eligible files · 199,008 chars · 4 model call(s) · 7 PR unit(s))

Whole diff with bounded context at base e7bb674 / head aa22c8e.
Excluded by noise policy: pnpm-lock.yaml.

🔍 Verified Adversarial Review Findings

🟡 IMPORTANT

  • apps/kernel/src/http.ts:1300: Authorization header bypassed by ?token= query parameter on all /api/* routes
    • Source Side: RIGHT (head)
    • Failure Trace: A request to GET /api/meetings//audio?token= with no Authorization header now authenticates: c.req.header('authorization') is null, so the new fallback builds Bearer <token> from the query string and calls authenticate(). Previously the same request was rejected. This exposes the bearer token to URL logs, proxy/CDN logs, Referer headers, and browser history, and lets any endpoint under /api/* accept a token in the query string. The meeting media endpoints are the new sensitive consumers of this widened surface.
    • Evidence: apps/kernel/src/http.ts:1300 at aa22c8ed4c91ec8afeffea0d5631e368805c6368, apps/kernel/src/http.ts:1294 at e7bb674c459f0ab1aa37ec330198ff7e9eb53264

🟡 IMPORTANT

  • modules/meetings/src/meetings.ts:250: Media key sealed with a hardcoded fallback key id when broker has no keyId
    • Source Side: RIGHT (head)
    • Failure Trace: In buildMeetingClaimMedia, const keyId = (deps.broker as { keyId?: string }).keyId ?? 'k1-default'; is used when the broker does not expose a keyId property. The wrapped key is then sealed under key_id='k1-default' in crove_meetings.media_key. At playback time (http.ts line 1665) the broker derives the wrap subkey via broker.deriveKey(media.keyId, ...) or broker.derive(...). If the real broker's current key id differs from 'k1-default', deriveKey('k1-default', ...) throws CredentialError('unknown key') and every meeting created through this path is permanently undecryptable (MEDIA_KEY_UNAVAILABLE 503). The fallback silently produces an envelope that no rotation or playback path can open.
    • Evidence: modules/meetings/src/meetings.ts:250 at aa22c8ed4c91ec8afeffea0d5631e368805c6368, apps/kernel/src/http.ts:1665 at aa22c8ed4c91ec8afeffea0d5631e368805c6368

1 possible issue(s) could not be grounded in or verified against the snapshot source, so they are not published.

Context investigation was bounded to the explicitly requested snapshot paths.

🔒 Dependency check

✅ Passed. Every new package version comes from the public registry or a source this lockfile already used for it, its checksum is pinned where the format records one, and OSV lists no new malicious package or high or critical vulnerability for the versions it was asked about.

Comment thread apps/kernel/src/http.ts Resolved
Comment thread modules/meetings/src/meetings.ts Resolved
@JOY

Copy link
Copy Markdown
Contributor Author

Security review (session)

  • Head reviewed: aa22c8ed4c91ec8afeffea0d5631e368805c6368
  • Reviewer: Independent fresh-context security reviewer (a80e6784, run by AI Meeting Notes Product Owner while #95 is pending)
  • Verdict: changes_requested

Blocking findings to fix before merge

  1. [P0-1] Mutual RLS policy recursion (SQLSTATE 42P17): meeting_read queries meeting_reader while meeting_reader_read and child policies on audio_segment, meeting_video, and meeting_reader query meeting. Break the cycle with narrow, context-bound SECURITY DEFINER helpers (is_meeting_recorder / is_meeting_reader).
  2. [P0-2] crove_meetings.media_key permissive policy: media_key is in MACHINERY (rls-matrix.db.test.ts), so replace apply_tenant_rls with ENABLE + FORCE ROW LEVEL SECURITY, REVOKE ALL, and SELECT crove_kernel.agent_write_guard_install('crove_meetings.media_key').
  3. [P0-3] Definer authorization & context binding:
    • media_key_seal must enforce crove_app, ctx_actor_type() = 'user', p_org_id = ctx_org_id(), full le_scope, active non-viewer membership, and recorder_user_id = ctx_user_id().
    • Bind is_meeting_reader, media_key_read, media_key_shred, meeting_owner_list, and meeting_owner_delete strictly to crove_kernel.ctx_org_id() (and ctx_user_id() where user-scoped); remove the system branch from media_key_read in M2.
    • Add OR crove_kernel.ctx_actor_type() IS NOT NULL to media_key_reseal_due, media_key_reseal, and media_key_counts on crove_worker.
  4. [P0-4] Query-string ?token= on /api/*: Revert apps/kernel/src/http.ts:1300 so bearer session JWTs are never accepted in URL query parameters; use a short-lived single-resource media playback ticket (or Authorization header) instead.
  5. [P1-1] 2 GiB media Range/claim memory & transaction bounds: Stream/slice encrypted 64 KiB chunks for HTTP Range requests (206 / 416), guard with the large concurrency limiter, avoid writing a new audit_event row on every Range scrub chunk, and move blob encryption/upload out of the open Postgres transaction in meetings.media.claim (deleting staging only after commit).
  6. [P1-2] Owner compliance delete (R3) and recorder-only meeting_update: Split unread owner compliance deletion into an owner-only R3 command (P11 fresh MFA), restrict meeting_owner_delete and non-recorder media_key_shred to owner (not admin), and restrict meeting_update RLS to recorder_user_id = ctx_user_id().
  7. [P1-3] Viewer exclusion on reader grants & demotion: Enforce ctx_actor_type() = 'user' and active role IN ('owner', 'admin', 'member') across all crove_meetings RLS policies, is_meeting_reader, meeting_reader_insert, meetings.meeting.create, and meetings.reader.add.
  8. [P1-4] CROVE_MODULES gating, upload purpose guard, and generation: Gate /api/meetings/:id/audio|video and uploadPurpose() on runtime.installed('meetings'), reject meeting_audio / meeting_video in POST /api/files/uploads/:id/complete so meeting uploads cannot bypass encryption, validate upload purpose/magic bytes in meetings.media.claim, and persist/pass BlobStore generation and maxBytes.

Also folding into the same fix commit: key buffer zeroization and fail-closed CredentialBroker.keyId (P2-1), media_key_reseal CAS check (P2-2), nosniff / CSP / CORP response headers (P2-3), explicit consent selection before staging (P3-1), and real PostgreSQL 17 integration tests (P3-2).

…nd MFA compliance

- P0-1 and P0-2: pin is_meeting_recorder and is_meeting_reader definers (SECURITY DEFINER, search_path = pg_catalog, pg_temp, crove_tenant only) and break recursive RLS

- P0-3: enable RLS on media_key with zero login grants and install agent_write_guard

- P0-4 and P1-1: strict bearer auth, 404 on uninstalled module and deleted meetings, HTTP 206 Range chunk streaming with deriveKey subkeys and security headers

- P1-2 and P1-3: require consent selection before meeting creation, validate active non-viewer members for readers, validate magic container bytes and staged max_bytes

- P1-4 and P3-1: split recorder delete (R1) and owner compliance delete (R3 with step-up MFA)

- P2-1 and P2-2: zeroize plaintext keys in finally, check generation in file storage, add CAS check in credential reseal

- P3-2: real PostgreSQL 17 integration test suite covering 7 security scenarios
dos[bot]
dos Bot approved these changes Nov 6, 2026

@dos dos Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⏱️ Code Review completed (53 eligible files · 247,973 chars · 2 model call(s) · 9 PR unit(s))

Whole diff with bounded context at base e7bb674 / head 1b8ce1f.
Excluded by noise policy: pnpm-lock.yaml.

✅ No verified defects

1 possible issue(s) could not be grounded in or verified against the snapshot source, so they are not published.

🔒 Dependency check

✅ Passed. Every new package version comes from the public registry or a source this lockfile already used for it, its checksum is pinned where the format records one, and OSV lists no new malicious package or high or critical vulnerability for the versions it was asked about.

@JOY

Copy link
Copy Markdown
Contributor Author

Security review (session)

Reviewed commit: 1b8ce1f366eec7c29002748901750be0dc07a1fb (Round 2 delta review against aa22c8ed94c19cf918e110796c6b3b36af92f11f)
Verdict: changes_requested

Round 1 blockers verified resolved in 1b8ce1f3:

  • Added crove_meetings.meeting_video and crove_meetings.media_key to org_purge (v_meetings_tables) with regression assertions in apps/kernel/test/meetings.db.test.ts.
  • Enforced crove_app session role and active non-viewer membership (crove_meetings.is_meeting_recorder) on meeting_create, audio_segment_create, meeting_video_upsert, meeting_delete, and media_key_seal.
  • Added strict container magic-byte verification (OggS, EBML WebM, RIFF....WAVE, MP3 ID3/sync, ISO BMFF ftyp) before sealing media keys, and capped video duration at 14,400 seconds across DB, Zod, and handler layers.
  • Aligned chunked AES-256-GCM AAD (orgId | fileId | chunkIndex | isFinal) and enforced chunk_count * 65536 ceiling before allocation.

Remaining blockers requested for commit 3:

  1. crove_meetings.media_key FORCE ROW LEVEL SECURITY (20261123120000_crove_meetings_core.sql:134): Remove ALTER TABLE crove_meetings.media_key FORCE ROW LEVEL SECURITY (relforcerowsecurity = false, relrowsecurity = true) so NOSUPERUSER NOBYPASSRLS migration/definer owners can seal, read, reseal, shred, and purge rows cleanly (migrate.db.test.ts).
  2. Bounded-memory Range playback and staged claim outside DB transaction (modules/meetings/src/meetings.ts, apps/kernel/src/http.ts, packages/kernel-runtime/src/s3-store.ts): Move staged blob download/encrypt/upload outside the open Postgres transaction (deleting staging only after commit), implement S3BlobStore.getRange, cap open-ended Range: bytes=0- windows with held(..., releaseLarge), and avoid writing duplicate sensitive: true WORM audit_event rows per scrub chunk.
  3. STORAGE=local (docker compose up) upload support and file_upload.purpose binding (apps/kernel/src/http.ts, 20261123115900_crove_kernel_file_meetings.sql): Support meeting media uploads when store.direct is undefined (STORAGE=local), persist and verify purpose on crove_kernel.file_upload so staged meeting media cannot be completed unencrypted under another purpose, and persist the sniffed container MIME type.
  4. Recorder active-membership check in media_key_shred (20261123120000_crove_meetings_core.sql:454): Gate the recorder branch on crove_meetings.is_meeting_recorder(v_org, p_meeting_id) (GRANT EXECUTE to crove_app, crove_worker), shred keys before meeting_owner_delete, and check broker.keyId === keyId on fallback derivation.

This branch was successfully deployed

1 active deployment
Preview – ludy — 1b8ce1f3 Deployed Nov 6, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Meetings M2: a meeting with its audio and video, encrypted, with the consent record

1 participant