Libra CodeHub

CedarCopilot/cedar-mail

Branch: staging

fix(security): authorize transcriptionKey against org before trusting it

merged#2742CedarCopilot

CedarCopilot wants to merge 5 commits into staging from fix/transcript-key-cross-org-auth

Live on prod, no production signal yetTimeline and evidence
  1. Opened
    Sep 9, 2026, 11:35 AM
  2. Sep 9, 2026, 7:07 PM
  3. Live on staging
    Sep 9, 2026, 7:22 PM
  4. Pipelines steady after this deploy
    Sep 9, 2026, 7:22 PM
  5. Merged
    Sep 9, 2026, 7:22 PM
  6. Live on prod
    Sep 9, 2026, 7:22 PM
  7. Observed 2 days
    Sep 9, 2026, 7:22 PM
  8. Pipelines steady after this deploy
    Sep 9, 2026, 7:22 PM
  9. Unobserved

    Live on prod, no production signal yet

    Sep 12, 2026, 11:54 AM

Behaviors Libra is checking

Conversation context-search indexing includes meeting transcripts stored under another same-organization member's connection by resolving each event's transcriptionKey.Degradedlow confidence

Strict CloudWatch fallback found 1 recent prod failure log lines for [processBufferedSlackWebhookEvents] Failed to process row (Failed query: delete from \), but the same failure had 48 log lines in the comparable pre-deploy baseline. Libra is not blaming this PR intent without.

staging, checked Sep 12, 2026, 7:22 PM
getConversation hydrates meetingData from the event's authoritative transcriptionKey even when the transcript was ingested through another same-organization member's connection.Degradedlow confidence

Strict CloudWatch fallback found 1 recent prod failure log lines for [processBufferedSlackWebhookEvents] Failed to process row (Failed query: delete from \), but the same failure had 48 log lines in the comparable pre-deploy baseline. Libra is not blaming this PR intent without.

staging, checked Sep 12, 2026, 7:22 PM
Agent action replay snapshots use organization-authorized transcriptionKey resolution so replayed meeting executions can load transcripts ingested by another member of the organization without exposing foreign-org content.Inconclusivelow confidence

Unable to verify the intended behavior: no production OTEL spans matched replay, snapshot, or event-execution surfaces since the 2026-09-10T02:22:47Z deploy. Positive runtime evidence is absent, so the result is inconclusive.

prod, checked Sep 12, 2026, 5:52 AM
Conversation file-store updates resolve each meeting through its event transcriptionKey and organization authorization, allowing same-org cross-user transcripts to be indexed for context search.Inconclusivehigh confidence

Since 2026-09-10T02:22:47Z, CloudWatch returned 0 logs for `[updateConversationTypeInFileStore]`, including no meeting-not-found warnings or direct update successes. OTEL returned 0 matching update/transcript/file-store update spans; the only related result was one unrelated.

prod, checked Sep 12, 2026, 6:54 AM
Re-executing a meeting event resolves its authoritative transcriptionKey against the acting user's organization instead of guessing only from the acting user's connections.Inconclusivelow confidence

Since 2026-09-10T02:22:47Z, queries for handleReexecuteEvent/reexecute-related OTEL spans and concrete CloudWatch messages returned 0 rows. The handler source confirms the meeting success message and storage-failure message, but there is no positive runtime execution evidence.

prod, checked Sep 12, 2026, 7:53 AM
Pre-execution event hydration receives the database and acting user context needed to authorize transcriptionKeys before restoring full meeting content for an agent run.Inconclusivelow confidence

Verification reached its 120s wall-clock budget before a tied verdict. Libra is keeping this intent verifying until the next check.

prod, checked Sep 12, 2026, 8:54 AM

Failures attributed to this change

No prod customers are affected while this is only in staging. If promoted, prod impact is unknown because Libra still needs a concrete exception, route, and failed user action before assigning severity. 1 hit · no retained affected-user count · no retained trace sample.Introducedmedium confidence

internal_only

staging, first seen Sep 9, 2026, 9:19 PM
No prod customers are affected while this is only in staging. If promoted, prod impact is unknown because Libra still needs a concrete exception, route, and failed user action before assigning severity. 1 hit · no retained affected-user count · no retained trace sample.Introducedmedium confidence

internal_only

staging, first seen Sep 9, 2026, 11:49 PM
No prod customers are affected while this is only in staging. If promoted, prod impact is unknown because Libra still needs a concrete exception, route, and failed user action before assigning severity. 0 hits · no retained affected-user count · no retained trace sample.Introducedmedium confidence

internal_only

staging, first seen Sep 10, 2026, 2:19 AM
No prod customers are affected while this is only in staging. If promoted, prod impact is unknown because Libra still needs a concrete exception, route, and failed user action before assigning severity. 0 hits · no retained affected-user count · no retained trace sample.Introducedmedium confidence

internal_only

staging, first seen Sep 10, 2026, 2:19 AM
No prod customers are affected while this is only in staging. If promoted, prod impact is unknown because Libra still needs a concrete exception, route, and failed user action before assigning severity. 1 hit · no retained affected-user count · no retained trace sample.Introducedmedium confidence

internal_only

staging, first seen Sep 10, 2026, 2:19 AM
No prod customers are affected while this is only in staging. If promoted, prod impact is unknown because Libra still needs a concrete exception, route, and failed user action before assigning severity. 1 hit · no retained affected-user count · no retained trace sample.Introducedmedium confidence

internal_only

staging, first seen Sep 10, 2026, 2:19 AM
No prod customers are affected while this is only in staging. If promoted, prod impact is unknown because Libra still needs a concrete exception, route, and failed user action before assigning severity. 1 hit · no retained affected-user count · no retained trace sample.Introducedmedium confidence

internal_only

staging, first seen Sep 10, 2026, 2:19 AM
No prod customers are affected while this is only in staging. If promoted, prod impact is unknown because Libra still needs a concrete exception, route, and failed user action before assigning severity. 1 hit · no retained affected-user count · no retained trace sample.Introducedmedium confidence

internal_only

staging, first seen Sep 10, 2026, 5:04 AM
No prod customers are affected while this is only in staging. If promoted, prod impact is unknown because Libra still needs a concrete exception, route, and failed user action before assigning severity. 1 hit · no retained affected-user count · no retained trace sample.Introducedmedium confidence

internal_only

staging, first seen Sep 10, 2026, 5:04 AM
No prod customers are affected while this is only in staging. If promoted, prod impact is unknown because Libra still needs a concrete exception, route, and failed user action before assigning severity. 0 hits · no retained affected-user count · no retained trace sample.Introducedmedium confidence

internal_only

staging, first seen Sep 10, 2026, 5:23 AM
No prod customers are affected while this is only in staging. If promoted, prod impact is unknown because Libra still needs a concrete exception, route, and failed user action before assigning severity. 22 hits · no retained affected-user count · no retained trace sample.Introducedmedium confidence

internal_only

staging, first seen Sep 10, 2026, 5:41 PM
No prod customers are affected while this is only in staging. If promoted, prod impact is unknown because Libra still needs a concrete exception, route, and failed user action before assigning severity. 22 hits · no retained affected-user count · no retained trace sample.Introducedmedium confidence

internal_only

staging, first seen Sep 10, 2026, 5:41 PM
No prod customers are affected while this is only in staging. If promoted, prod impact is unknown because Libra still needs a concrete exception, route, and failed user action before assigning severity. 9 hits · no retained affected-user count · no retained trace sample.Introducedmedium confidence

internal_only

staging, first seen Sep 10, 2026, 5:41 PM

Libra has verdicts on 0 of 9 tracked behaviors on prod; 9 are still being checked. Libra checks hourly for 3 days after each deploy.

Summary

Follow-up to #2740 (merged), fixing a cross-org security gap Greptile flagged on that PR's review.

#2740 fixed cross-user meeting-transcript resolution by trusting crm_meeting_events.transcription_key directly instead of guessing an S3 key from the querying user's own connections. But transcription_key is client-writable via crm.createEvent with no ownership check at write time , so trusting it unconditionally let any authenticated caller read any organization's meeting transcript, just by guessing or leaking a key. This gap has been live in production since #2740 merged.

  • isTranscriptionKeyAuthorizedForOrg (services/crm/events.ts) parses the connection id out of a transcriptionKey, resolves that connection's owning user's organizationId, and checks it against the reading event's org. Fails closed (false) when db or organizationId is missing.
  • resolveMeetingS3Object now calls this before trusting a direct key; a rejected key falls through to the existing guessed-connection loop exactly as if no key had been recorded , which can only ever find a meeting under a connection the querying user already owns, so it cannot leak another org's transcript.
  • getEventRawContentByIds and hydrateExecutionEvents now resolve and thread the acting organizationId through. Both callers of hydrateExecutionEvents (pre-execution-setup.ts, initial-sync/handlers.ts) pass the db/userId they already had in scope.
  • getMeetingTranscription (trpc/routes/crm.ts) had the same underlying gap via a raw client-supplied transcriptionKey , this one predates #2740. It's a small, self-contained reuse of isTranscriptionKeyAuthorizedForOrg with the same fall-through-to-connection-lookup behavior.
  • cedarMeetingDataSchema (services/integrations/meetings/types.ts) replaces the as CedarMeetingData casts at both S3-JSON read boundaries with real zod validation (per root CLAUDE.md → "NEVER cast"). meetingTime accepts either a Date or an ISO string since S3-stored data round-trips as JSON, and normalizes to Date.

Test plan

  • pnpm --filter @zero/server run types , clean
  • pnpm --filter @zero/server exec vitest run src/services/crm/__tests__/meeting-s3-cross-user-resolution.test.ts , updated for resolveMeetingS3Object's new db/organizationId params; adds cross-org rejection, fail-closed (no db/no orgId), and isTranscriptionKeyAuthorizedForOrg unit coverage (19 tests)
  • pnpm --filter @zero/server exec vitest run src/services/crm/__tests__/transcript-key-cross-org-auth.test.ts , new end-to-end coverage for getEventRawContentByIds and hydrateExecutionEvents: cross-org rejection (no disclosure), legitimate same-org cross-user resolution (the original #2740 case), and malformed S3 JSON now caught by cedarMeetingDataSchema.parse and handled the same way as any other S3-read failure at each site (7 tests)
  • Full src/services/crm/__tests__/ suite (70 files, 805 tests) , all pass
  • eslint on every touched file , clean (one pre-existing unrelated no-explicit-any finding elsewhere in crm.ts, untouched by this diff)

🤖 Generated with Claude Code

https://claude.ai/code/session_01YZB5RgfxHLmuKtWof2wP4g

Greptile Summary

This PR adds organization authorization before direct meeting-transcript object reads, threads database and acting-user context through hydration callers, and validates stored meeting JSON with Zod.

  • Resolves the key owner through its connection and compares organization ownership before accessing object storage.
  • Falls back to caller-owned connection lookups when a direct key is rejected.
  • Adds cross-organization and malformed-payload regression coverage.
  • Currently breaks manually uploaded transcripts because those objects use a user-ID rather than connection-ID key prefix.

Confidence Score: 4/5

The PR is not ready to merge because its new authorization

Show production surfaces and changed-file mapping

Production surfaces

Libra has not measured any production surfaces for this change yet.

Changed files → surfaces

  • apps/server/scripts/dump-followup-emails.tsno production surface mapped
  • apps/server/src/mastra/routeHandlers/event-execution/handleReexecuteEvent.tsno production surface mapped
  • apps/server/src/mastra/workflows/event-execution/pre-execution-setup.tsno production surface mapped
  • apps/server/src/services/agent-action-queue/replay-execution.tsno production surface mapped
  • apps/server/src/services/context-search/documents.tsno production surface mapped
  • apps/server/src/services/crm/__tests__/conversations.get-conversation-meeting-transcript-key.test.tsno production surface mapped
  • apps/server/src/services/crm/__tests__/meeting-s3-cross-user-resolution.test.tsno production surface mapped
  • apps/server/src/services/crm/__tests__/transcript-key-cross-org-auth.test.tsno production surface mapped
  • apps/server/src/services/crm/conversations.tsno production surface mapped
  • apps/server/src/services/crm/events.tsno production surface mapped
  • apps/server/src/services/crm/taxonomy-extraction-step.tsno production surface mapped
  • apps/server/src/services/integrations/meetings/types.tsno production surface mapped
  • apps/server/src/services/mail/initial-sync/handlers.tsno production surface mapped
  • apps/server/src/services/turbopuffer/meeting-timestamps.tsno production surface mapped
  • apps/server/src/trpc/routes/admin.tsno production surface mapped
  • apps/server/src/trpc/routes/crm.tsno production surface mapped