Skip to content

[WIP] feat: add meeting-baas-recorder Twenty app - #1

Open
Lazare-42 wants to merge 5 commits into
mainfrom
feat/meeting-baas-recorder-v1
Open

[WIP] feat: add meeting-baas-recorder Twenty app#1
Lazare-42 wants to merge 5 commits into
mainfrom
feat/meeting-baas-recorder-v1

Conversation

@Lazare-42

Copy link
Copy Markdown
Contributor

Summary

New Twenty app: meeting-baas-recorder — automatically records meetings via Meeting BaaS bots dispatched from calendar events.

  • Recording custom object with fields for bot ID, transcript, duration, speakers, platform, mp4/audio URLs, and relations to CalendarEvent, Company, Person, WorkspaceMember
  • Per-user recording preference (SELECT field on WorkspaceMember): Record All / Record Organized Only / Record None
  • Calendar event triggers (calendarEvent.created + calendarEvent.updated) that resolve the event owner, check their preference, and call createScheduledBot via the Meeting BaaS V2 API
  • Webhook handler for bot.completed events — transforms transcript data and upserts the Recording via REST API
  • Settings front component with API key status indicator, calendar connection banner, and recording preference radio selector
  • Views: All Recordings, Completed Recordings, By Platform

Architecture

Calendar Event (created/updated with conference link)
  → resolve owner → check recording preference → createScheduledBot()
  → Meeting BaaS records the call
  → bot.completed webhook → WebhookHandler → syncBotRecording → upsert Recording

Open items (WIP)

  • Widget on calendar event record page — blocked on Twenty custom layouts shipping
  • LLM summary generation — pending discussion with Martin on whether apps can access Twenty's AI
  • Billing integration — V2 (unified billing through Twenty credits)

Test plan

  • npx tsc --noEmit in meeting-baas-recorder directory
  • twenty app build discovers all exported objects, fields, views, logic functions, and front components
  • Create calendar event with Google Meet link → verify on-calendar-event-created trigger fires and calls createScheduledBot
  • Receive bot.completed webhook → verify Recording is created and linked to calendar event
  • Settings tab renders with API key status, calendar banner, and preference radio buttons

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Apr 8, 2026

Copy link
Copy Markdown

Summary by CodeRabbit

  • New Features
    • Added Meeting BaaS Recorder application for managing and syncing meeting recordings
    • Introduced recording management interface with automated synchronization from Meeting BaaS
    • Added recording preferences (Record All, Organizer Only, None) for workspace members
    • Added calendar event-triggered recording automation
    • Added organized recording views filtered by platform and completion status
    • Enhanced application settings with improved layout rendering support

Walkthrough

This PR introduces the Meeting BaaS Recorder application, a new community app that enables automated recording of calendar meetings, webhook-based transcript and metadata sync, and user-facing settings UI. The app integrates with the Twenty SDK to define recording objects, fields, relations, logic functions, views, and webhook handling.

Changes

Cohort / File(s) Summary
Configuration & Tooling
.env.example, .gitignore, package.json, tsconfig.json, jest.config.mjs, project.json
Added foundational config files for environment variables, dependencies (Meeting BaaS SDK, axios, TypeScript), Jest testing setup with ts-jest, and Nx build/test/lint targets.
Application & Role Setup
src/application-config.ts, src/roles/default.role.ts, src/constants/universal-identifiers.ts
Defined application metadata, role-based access control with object-level recording permissions, and reusable universal identifier constants.
Recording Object & Standard Field Extensions
src/objects/recording.ts, src/objects/index.ts, src/fields/bot-name-on-workspace-member.field.ts, src/fields/bot-entry-message-on-workspace-member.field.ts, src/fields/recording-preference-on-workspace-member.field.ts
Created recording object schema with fields (name, bot ID, date, duration, transcript, URLs, platform, status), added three custom fields to workspace member for bot configuration and recording preferences, with SELECT field for preference options.
Relation Fields
src/fields/calendar-event-on-recording.field.ts, src/fields/recordings-on-calendar-event.field.ts, src/fields/workspace-member-on-recording.field.ts, src/fields/recordings-on-workspace-member.field.ts
Established bidirectional many-to-one and one-to-many relations between recordings and calendar events, and between recordings and workspace members (owners).
Logic Functions & Webhook Handling
src/logic-functions/on-calendar-event-created.ts, src/logic-functions/on-calendar-event-updated.ts, src/logic-functions/schedule-bot.ts, src/logic-functions/batch-schedule-bots.ts, src/receive-recording-webhook.ts
Implemented database event triggers (calendar event creation/update) that conditionally schedule bots, batch scheduling endpoint for retroactive meeting recording setup, and unauthenticated webhook route for bot completion callbacks.
Core Services & API Integration
src/meeting-baas-api-client.ts, src/twenty-sync-service.ts, src/webhook-handler.ts, src/webhook-validator.ts, src/logger.ts, src/utils.ts
Built API client for Meeting BaaS SDK operations (scheduled bot creation, batch operations), REST sync layer for recording CRUD and calendar event ownership resolution, webhook parsing/validation with Zod, event handler orchestration, configurable logging, and HTTP utilities.
AI & Summary Generation
src/generate-summary.ts
Added async summary generation via Twenty's AI endpoint, non-fatal fallback to null on failure.
Settings UI Component & Views
src/front-components/meeting-baas-settings.front-component.tsx, src/views/all-recordings.view.ts, src/views/by-platform.view.ts, src/views/completed-recordings.view.ts, src/navigation-menu-items/recordings.navigation-menu-item.ts
Created Emotion-styled React settings panel showing API key/calendar connection status, recording preference radio options, banner alerts, and batch-schedule-bots button; defined three recordings views (all, by-platform Kanban, completed-only) and navigation menu entry.
Type Definitions & Exports
src/types.ts, src/index.ts
Centralized webhook payload types, meeting platform detection enum, recording/sync data shapes, and re-exported all public module APIs (objects, fields, components, logic functions, services, types) from single entrypoint.
Tests
src/__tests__/app-install.integration-test.ts, src/__tests__/setup-test.ts, src/receive-recording-webhook.test.ts, src/twenty-sync-service.test.ts, src/webhook-handler.test.ts, src/webhook-validator.test.ts
Added integration test for app build/deploy/install flow, test setup helpers for environment validation, and unit tests covering webhook validation, handler orchestration, sync service fallbacks, and validator edge cases.
Shims & Type Enums
src/shims/twenty-shared/types.ts
Defined local shim enums (ViewType, ViewKey, ViewFilterOperand) to support view manifest definitions without direct dependency on twenty-shared.
Twenty Framework Modifications
packages/twenty-front/src/pages/settings/applications/tabs/SettingsApplicationCustomTab.tsx, packages/twenty-server/src/engine/.../application-manifest-migration.service.ts, packages/twenty-server/src/engine/.../from-view-manifest-to-universal-flat-view.util.ts, packages/twenty-shared/src/application/viewManifestType.ts
Wrapped custom tab renderer with LayoutRenderingProvider for dashboard layout context, fixed permission flag payload mapping in role sync, propagated Kanban mainGroupByFieldMetadataUniversalIdentifier from manifest, and extended ViewManifest type with optional group-by field identifier.

Sequence Diagram(s)

sequenceDiagram
    participant CalEvent as Calendar Event
    participant LogicFunc as Logic Function<br/>(on-calendar-event-created/updated)
    participant Schedule as scheduleBot()
    participant BaasAPI as MeetingBaasApiClient
    participant TwentyREST as Twenty REST API
    participant Recording as Recording Object
    
    CalEvent->>LogicFunc: calendarEvent.created/updated
    LogicFunc->>LogicFunc: Validate conference link & start time
    LogicFunc->>Schedule: scheduleBot(eventId, conferenceUrl, startsAt)
    Schedule->>TwentyREST: Check MEETING_BAAS_API_KEY
    Schedule->>TwentyREST: Query recording preference<br/>(workspaceMember)
    Schedule->>TwentyREST: Resolve calendar event owner
    Schedule->>TwentyREST: Check if recording exists<br/>(deduplication)
    Schedule->>BaasAPI: createScheduledBot(meetingUrl, botName, extra)
    BaasAPI-->>Schedule: botId
    Schedule->>TwentyREST: upsertRecording(status: IN_PROGRESS)
    TwentyREST->>Recording: Create/Update recording
    Recording-->>TwentyREST: recordingId
    Schedule-->>LogicFunc: botId
    LogicFunc-->>CalEvent: { scheduled: true, botId }
Loading
sequenceDiagram
    participant MeetingBaaS as Meeting BaaS Service
    participant Webhook as POST /webhook/meeting-baas
    participant Handler as WebhookHandler
    participant BaasAPI as MeetingBaasApiClient
    participant Summary as generateSummary()
    participant Sync as syncBotRecording()
    participant TwentyREST as Twenty REST API
    
    MeetingBaaS->>Webhook: bot.completed event
    Webhook->>Handler: handle(payload, headers)
    Handler->>Handler: Verify API key (x-mb-secret)
    Handler->>BaasAPI: transformWebhookData(botData, extra)
    BaasAPI->>BaasAPI: Fetch transcript (diarization/transcription)
    BaasAPI-->>Handler: recordingData
    Handler->>Summary: generateSummary(transcript)
    Summary-->>Handler: summary
    Handler->>Sync: syncBotRecording(recordingData, result)
    Sync->>TwentyREST: upsertRecording(mp4Url, meetingUrl, etc.)
    TwentyREST-->>Sync: recordingId
    Sync-->>Handler: { recordingsProcessed, recordingsCreated }
    Handler-->>MeetingBaaS: { success: true, recordingId }
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 Hops with glee—the bots record our meetings now,
Transcripts bloom in Twenty's garden, oh how!
Webhooks ping, recordings sync with care,
One app to capture every meeting there. 🎥✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The PR title '[WIP] feat: add meeting-baas-recorder Twenty app' accurately describes the main change—adding a new Twenty app for recording meetings via Meeting BaaS.
Description check ✅ Passed The PR description is comprehensive and directly related to the changeset, covering the new Recording object, calendar triggers, webhook handler, settings UI, and implementation architecture.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/meeting-baas-recorder-v1

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

Comment @coderabbitai help to get the list of available commands and usage tips.

@Lazare-42
Lazare-42 force-pushed the feat/meeting-baas-recorder-v1 branch from acdf49e to 3f11392 Compare April 8, 2026 09:00
Recording custom object linked to CalendarEvent and WorkspaceMember.
Calendar event triggers for automatic bot scheduling based on per-user
recording preferences (all/organizer-only/none). Settings front component
with API key status, calendar connection banner, and preference selector.
Webhook handler stores completed recordings via REST API, using the bot's
extra field to link back to calendar event and workspace member.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@Lazare-42
Lazare-42 force-pushed the feat/meeting-baas-recorder-v1 branch from 3f11392 to 3a015d7 Compare April 8, 2026 09:21

@martmull martmull 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.

Hey @Lazare-42, I don't succeed to install the app. There is imports from twenty-shared which should not exist. Plus you are using a 0.1.3 version of the twenty-sdk which is very very old (e are currently at 0.9.0 -> https://www.npmjs.com/package/twenty-sdk)

  • I think you need to create a dedicated repository for your app, and not commit it in a twenty repository. You will avoid issues like importing stuff from twenty-shared
  • you need to start from scratch with a clean scaffolded app using the npx create-twenty-app@latest meeting-bass-recorder command. Then you will be able to adapt your current app to the 0.9.0 version

I am here to help if you need

Image

@Lazare-42

Copy link
Copy Markdown
Contributor Author

Hey @Lazare-42, I don't succeed to install the app. There is imports from twenty-shared which should not exist. Plus you are using a 0.1.3 version of the twenty-sdk which is very very old (e are currently at 0.9.0 -> https://www.npmjs.com/package/twenty-sdk)

  • I think you need to create a dedicated repository for your app, and not commit it in a twenty repository. You will avoid issues like importing stuff from twenty-shared
  • you need to start from scratch with a clean scaffolded app using the npx create-twenty-app@latest meeting-bass-recorder command. Then you will be able to adapt your current app to the 0.9.0 version

I am here to help if you need

Image

Thanks a ton, patching that today!

Lazare-42 and others added 4 commits April 16, 2026 23:16
Syncs all fixes and features from the standalone meeting-baas-recorder
repository (source of truth):

- Fix REST response unwrapping (recordingPreference, calendarChannel,
  connectedAccount, workspaceMember lookups)
- Add bot name/entry message settings and pass to Meeting BaaS API
- Add bot.status_change webhook handling
- Add batch-schedule-bots logic function
- Add AI summary generation for transcripts
- Add tests for webhook handler, validator, and sync service
- Fix absolute imports, ViewKey enum shim

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai 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.

Actionable comments posted: 42

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/twenty-apps/community/meeting-baas-recorder/jest.config.mjs`:
- Around line 9-12: The project is configured with Jest (jest.config.mjs) but
the new test files (e.g., src/receive-recording-webhook.test.ts) import from
vitest, causing runtime resolution failures; fix by switching to Vitest: replace
jest.config.mjs with an equivalent Vitest config (vite or vitest config file),
update package.json "test" script to run vitest, and adjust devDependencies to
include vitest (and remove ts-jest/jest if not needed); alternatively, if you
prefer Jest, update src/receive-recording-webhook.test.ts to remove vitest
imports and use Jest globals (convert assertions/mocks to Jest equivalents) and
ensure package.json and devDependencies remain configured for Jest.

In `@packages/twenty-apps/community/meeting-baas-recorder/package.json`:
- Around line 5-9: The package.json engines entry is too restrictive ("node":
"^24.5.0") and can cause EBADENGINE failures; update the "engines" field in this
package (the engines object in
packages/twenty-apps/community/meeting-baas-recorder/package.json) to match the
monorepo-wide Node version policy (for example use the same semver range as the
root package.json like "node": ">=18" or the exact range used in the repo) and
adjust the "yarn" entry if the monorepo requires a different Yarn major; ensure
you only change the engines values (the engines object) so local installs and CI
use the monorepo-approved Node/Yarn range.

In
`@packages/twenty-apps/community/meeting-baas-recorder/src/__tests__/app-install.integration-test.ts`:
- Around line 28-29: The test uses a non-null assertion on
buildResult.data.tarballPath which can pass undefined into appDeploy; instead
check the build result discriminant and the tarball path explicitly before
calling appDeploy—e.g., assert buildResult.success is true (or throw/fail the
test if not) and verify buildResult.data?.tarballPath is defined, then pass that
verified value to appDeploy (reference: buildResult, buildResult.data,
tarballPath, and appDeploy).
- Around line 48-56: The teardown calls appUninstall unconditionally in afterAll
which can run even if beforeAll failed; make uninstall conditional by tracking
install success (e.g., a boolean like let installed = false set to true in the
successful install path inside beforeAll or the test setup) and only call
appUninstall when installed is true, or update appUninstall to be
idempotent/no-op on non-existent apps; locate afterAll and the install logic
(beforeAll / install function) and add the install flag check before invoking
appUninstall (or make appUninstall tolerate missing installs).

In
`@packages/twenty-apps/community/meeting-baas-recorder/src/__tests__/setup-test.ts`:
- Around line 38-52: The written config file CONFIG_PATH is created with default
permissions exposing the API key; change the fs.writeFileSync call to pass a
mode of 0o600 so the file is user-readable/writable only, and after creating
CONFIG_DIR with fs.mkdirSync consider calling fs.chmodSync(CONFIG_DIR, 0o700) to
tighten directory permissions; update the setup-test.ts code that uses
fs.mkdirSync and fs.writeFileSync (look for CONFIG_DIR and CONFIG_PATH) to
include these permission changes.
- Around line 12-21: Remove the misleading non-null assertions on apiUrl and
token: don't use process.env.TWENTY_API_URL! or
process.env.TWENTY_API_KEY!—declare them as possibly undefined (const apiUrl =
process.env.TWENTY_API_URL; const token = process.env.TWENTY_API_KEY;) keep the
existing runtime check (if (!apiUrl || !token) throw ...), and then rely on the
narrowed types for apiUrl and token afterwards (or assign them to new consts
like apiUrlVal/tokenVal after the check) so TypeScript knows they are strings
without using !; update any references to use the narrowed variables (apiUrl,
token or the new consts).

In
`@packages/twenty-apps/community/meeting-baas-recorder/src/application-config.ts`:
- Line 16: Replace the hardcoded UUID assigned to
settingsCustomTabFrontComponentUniversalIdentifier with the exported
SETTINGS_FRONT_COMPONENT_ID constant: import the SETTINGS_FRONT_COMPONENT_ID
from its module where it is defined and set
settingsCustomTabFrontComponentUniversalIdentifier = SETTINGS_FRONT_COMPONENT_ID
(instead of the raw UUID) so the identifier is sourced from the single exported
constant.

In
`@packages/twenty-apps/community/meeting-baas-recorder/src/fields/bot-entry-message-on-workspace-member.field.ts`:
- Around line 17-18: The field claims a "max 500 characters" but no enforcement
is present; add validation to prevent oversized messages before calling the
Meeting BaaS API. Update the universalSettings entry in
bot-entry-message-on-workspace-member.field.ts to include a maxLength: 500 (or
equivalent validation rule) for the field, and/or add a pre-flight check in
schedule-bot.ts that inspects botEntryMessage before invoking
createScheduledBot: if botEntryMessage.length > 500 throw/return a clear error
or truncate per policy. Ensure the check references the same field name
(botEntryMessage) and runs prior to the createScheduledBot call so the API never
receives unvalidated input.

In
`@packages/twenty-apps/community/meeting-baas-recorder/src/front-components/meeting-baas-settings.front-component.tsx`:
- Around line 352-363: In handlePreferenceChange, capture the current preference
into a local variable (e.g., prevPreference = preference ??
member.recordingPreference ?? 'RECORD_NONE') before calling
setPreference(newPreference) so the optimistic update can be rolled back
correctly; on failure inside the catch, call setPreference(prevPreference)
instead of using member.recordingPreference, and keep the existing
updateWorkspaceMember, setIsSaving, and isSaving logic intact to ensure correct
save/rollback behavior.
- Around line 365-390: handleBatchSchedule currently calls response.json()
unconditionally and treats any response as success; update handleBatchSchedule
to check response.ok after fetch, and if not ok parse the response body (try
json then text) to extract an error message and call setBatchResult with
scheduled:0, skipped:0, errors:[extractedMessage] and hasMore:false; only when
response.ok parse the success payload and set scheduled/skipped/errors/hasMore
as before. Use the existing symbols handleBatchSchedule, setBatchResult,
getApiUrl, getToken and ensure the catch block still handles network errors.
- Around line 293-322: checkApiKeyConfigured currently queries
findManyApplications and scans all apps looking for MEETING_BAAS_API_KEY, then
uses the fragile SECRET_VARIABLE_MASK string comparison to infer configuration;
update the GraphQL query used in checkApiKeyConfigured to scope to the
meeting-baas-recorder app (e.g. filter/find by name "meeting-baas-recorder"
instead of findManyApplications) and request an explicit flag from the API such
as hasValue or isConfigured for applicationVariables so you can return that flag
directly; if the backend cannot provide a flag immediately, make the heuristic
more robust (e.g. request the variable metadata and treat any value whose length
is greater than SECRET_VARIABLE_MASK.length or that does not end with
SECRET_VARIABLE_MASK as configured) and replace the current value !==
SECRET_VARIABLE_MASK check with that new logic using MEETING_BAAS_API_KEY and
SECRET_VARIABLE_MASK in checkApiKeyConfigured.

In
`@packages/twenty-apps/community/meeting-baas-recorder/src/generate-summary.ts`:
- Around line 21-44: The axios POST in generate-summary.ts must include a
request timeout and surface errors instead of silently swallowing them: add an
axios timeout (e.g. 8–10s) to the options passed to axios(...) and change the
catch block to log the caught error (use console.error or the module's logger)
with contextual info including getApiUrl(), token presence, and that it was
generating a summary; also truncate the transcript variable before sending
(e.g., enforce a max character/token limit) so very large transcripts are
shortened prior to passing userPrompt and SYSTEM_PROMPT to the AI endpoint to
avoid token-limit errors and wasted round-trips.

In `@packages/twenty-apps/community/meeting-baas-recorder/src/logger.ts`:
- Around line 11-16: The module-level currentLevel is computed once at import
using parseLogLevel(process.env.LOG_LEVEL), so runtime changes to
process.env.LOG_LEVEL (e.g., in tests) are ignored; change to lazy evaluation by
removing the frozen currentLevel and instead call
parseLogLevel(process.env.LOG_LEVEL || 'error') inside shouldLog (or add a
getCurrentLevel helper used by shouldLog) so LOG_LEVELS checks use the
up-to-date env value; update any references to currentLevel to use the new
helper or inline call and keep parseLogLevel and LOG_LEVELS unchanged.
- Around line 34-36: The current critical logger implementation bypasses
shouldLog so LOG_LEVEL=silent still prints; either gate critical using the same
check as other levels (call shouldLog before executing the critical handler) or
add a new numeric log level above "error" (e.g., "critical" in the logLevels
map) and update shouldLog to respect it; modify the critical function (and
related logLevels/shouldLog logic) so critical is evaluated consistently with
other levels rather than unconditionally calling console.error.

In
`@packages/twenty-apps/community/meeting-baas-recorder/src/logic-functions/batch-schedule-bots.ts`:
- Around line 170-208: The current serial loop over events causes an N+1 HTTP
call storm (checkIfRecordingExistsForEvent, resolveCalendarEventOwner,
fetchWorkspaceMemberPreference, isOrganizer) and may hit the 60s timeout;
refactor batch-schedule-bots.ts to (1) pre-batch the dedup/ownership lookups by
calling a new or existing backend method once with events.map(e=>e.id) instead
of calling checkIfRecordingExistsForEvent per event, (2) parallelize remaining
per-event work in controlled concurrency (use Promise.all with a chunk/pool)
when resolving preferences and isOrganizer, (3) add a cache for isOrganizer
results keyed by calendarEventId similar to preferenceCache, and (4) ensure you
still skip events when ownership.workspaceMemberId is missing and increment
result.skipped; operate on the same qualified push ({ ...event,
workspaceMemberId }) logic after these changes (target symbols:
checkIfRecordingExistsForEvent, resolveCalendarEventOwner,
fetchWorkspaceMemberPreference, isOrganizer, preferenceCache, qualified).
- Around line 120-127: The current hasMore logic uses events.length >= maxEvents
which can be a false positive; change the function that returns { events,
hasMore } to derive hasMore from whether the last fetched page was full
(page.length === pageSize) instead of total events. Introduce a boolean like
lastPageFull (set true when a fetched page has length === pageSize, false when
page.length < pageSize and you break), update the loop to set lastPageFull
before breaking (using the existing page and pageSize variables and cursor
handling), and return hasMore = lastPageFull rather than basing it on
events.length or maxEvents.
- Around line 230-260: The loop assumes positional alignment between botIds and
batch but the SDK returns successful items with their original index, causing
mis-mapping; update the code that handles the batchCreateScheduledBots response
(the call to client.batchCreateScheduledBots and the subsequent
placeholder-creation loop) to iterate over the SDK's successful result entries
(use the returned index field for each successful item) to look up the correct
event from batch[index] and use that entry's workspaceMemberId/calendarEventId
when calling upsertRecording (instead of using botIds[j] with batch[j]); also
increment result.scheduled only for each successfully-mapped item and retain the
existing error mapping using errors[].index.

In
`@packages/twenty-apps/community/meeting-baas-recorder/src/logic-functions/on-calendar-event-created.ts`:
- Around line 22-48: Add a guard in the handler function to skip scheduling for
events whose startsAt is in the past: after extracting startsAt in
on-calendar-event-created.ts, parse/compare startsAt against current time
(Date.now()) and return { skipped: true, reason: 'event in the past' } if
startsAt <= now (or reuse the project’s past-event utility if available). This
prevents calling scheduleBot (and its downstream checkIfRecordingExistsForEvent
/ Meeting BaaS API) for historical events while keeping the existing error
handling and return shapes intact.

In
`@packages/twenty-apps/community/meeting-baas-recorder/src/logic-functions/on-calendar-event-updated.ts`:
- Around line 27-42: hasExistingRecording currently swallows all errors and
returns false, which causes transient lookup failures to trigger scheduleBot and
create duplicate recordings; change hasExistingRecording to distinguish "no
recording" from "lookup failed" by returning a result object (e.g. { exists:
boolean, error?: string }) or by throwing the error, include the underlying
error message when failing the lookup, and update the caller in
onCalendarEventUpdated (where scheduleBot is invoked) to bail out or return {
skipped: true, reason: 'dedup check failed: <msg>' } when the lookup failed
rather than proceeding to scheduleBot.
- Line 24: The module currently captures TWENTY_API_KEY at import time causing
stale/empty auth; change API calls in hasExistingRecording() (and any other
places using TWENTY_API_KEY) to call the existing restHeaders() utility at
runtime to build headers so env changes are picked up; also include 'startsAt'
in the updatedFields array used by onCalendarEventUpdated and update the handler
logic so when hasExistingRecording() finds an existing recording you reschedule
the bot's join time based on the new startsAt (instead of skipping), ensuring
reschedule code paths run for conferenceLink-only updates and startsAt-only
updates alike.

In
`@packages/twenty-apps/community/meeting-baas-recorder/src/logic-functions/schedule-bot.ts`:
- Around line 132-147: Currently the code passes the long-lived Meeting BaaS API
key (apiKey / MEETING_BAAS_API_KEY) as the webhook callbackSecret in
MeetingBaasApiClient.createScheduledBot; instead generate or fetch a dedicated
webhook secret (e.g., application variable MEETING_BAAS_WEBHOOK_SECRET), pass
that value as callbackSecret (not apiKey) in createScheduledBot, and update
verifyWebhookApiKey to validate incoming webhooks against the dedicated
MEETING_BAAS_WEBHOOK_SECRET rather than the API key. Ensure the new secret is
generated/stored separately from the API key and used only for webhook
verification to allow independent rotation.

In
`@packages/twenty-apps/community/meeting-baas-recorder/src/meeting-baas-api-client.ts`:
- Around line 137-177: fetchTranscript lacks network and read limits; wrap the
fetch call in an AbortController with a configurable timeout (e.g., create
controller, setTimeout to controller.abort after N ms, pass controller.signal to
fetch) and enforce a maximum response size when reading the body (do not call
response.text() directly for large/unknown payloads). Replace response.text()
with streaming reads from response.body.getReader() (or use response.arrayBuffer
with a size check), accumulate bytes up to a MAX_TRANSCRIPT_BYTES constant,
abort the controller and log/return early if the cap is exceeded, and always
clear the timeout and release the controller on success/error; refer to
fetchTranscript, the fetch(...) call, and response.text() usages to locate where
to add the AbortController, timeout, and streaming size cap logic.
- Around line 92-108: The code in createBatchScheduledBots (where you call
this.client.batchCreateScheduledBots) flattens result.data into botIds and loses
alignment with the original params.items order; change the logic to return an
aligned result array the same length/order as the input batch so callers can zip
safely. Specifically, build an array (named e.g. alignedBotIds or
alignedResults) of length params.items.length, iterate result.data and place
each success entry at its original input index (use the returned index field if
present, otherwise match by a stable key), and convert result.errors into
per-index error entries so the method returns { botIds: alignedBotIds, errors:
alignedErrors } where positions correspond to input items; update references to
botIds/errors accordingly (function: batchCreateScheduledBots result handling in
meeting-baas-api-client).

In `@packages/twenty-apps/community/meeting-baas-recorder/src/objects/index.ts`:
- Around line 1-13: The barrel export is missing SUMMARY_FIELD_ID: update the
export list that currently re-exports RECORDING_UNIVERSAL_IDENTIFIER,
NAME_FIELD_ID, BOT_ID_FIELD_ID, DATE_FIELD_ID, DURATION_FIELD_ID,
TRANSCRIPT_FIELD_ID, MEETING_URL_FIELD_ID, MP4_URL_FIELD_ID, PLATFORM_FIELD_ID,
STATUS_FIELD_ID and RecordingObject to also include SUMMARY_FIELD_ID so
consumers importing from this module can access SUMMARY_FIELD_ID just like the
other field IDs.

In
`@packages/twenty-apps/community/meeting-baas-recorder/src/objects/recording.ts`:
- Around line 99-110: The status select field (universalIdentifier
STATUS_FIELD_ID, type FieldType.SELECT, name 'status') lacks a defaultValue and
lists IN_PROGRESS last; add defaultValue: 'IN_PROGRESS' to the field definition
and reorder the options so the IN_PROGRESS option (id
'12374b43-a6fe-48d6-8a71-437242e59df2') is position 0, with COMPLETED and FAILED
shifted to positions 1 and 2 respectively, ensuring the default reflects the
recording lifecycle and positions are updated accordingly.
- Around line 35-85: The schema fields for BOT_ID_FIELD_ID, TRANSCRIPT_FIELD_ID,
SUMMARY_FIELD_ID, MEETING_URL_FIELD_ID, MP4_URL_FIELD_ID and DURATION_FIELD_ID
should be marked nullable and given null defaults to avoid forcing empty
strings/zeros when a Recording row is created before the bot completes; update
the field objects where universalIdentifier === BOT_ID_FIELD_ID,
TRANSCRIPT_FIELD_ID, SUMMARY_FIELD_ID, MEETING_URL_FIELD_ID, MP4_URL_FIELD_ID
and DURATION_FIELD_ID to include isNullable: true and add defaultValue: null
(for DURATION_FIELD_ID you can set defaultValue: null instead of 0) so the
initial record can store unknown values until syncBotRecording/bot.completed
populates them.

In
`@packages/twenty-apps/community/meeting-baas-recorder/src/receive-recording-webhook.test.ts`:
- Around line 1-17: The test imports Vitest helpers but the package uses Jest;
remove the explicit import from 'vitest' in receive-recording-webhook.test.ts
and rely on Jest's global describe/it/expect (i.e., delete the line "import {
describe, expect, it } from 'vitest'") so the test runs under the existing
Jest/ts-jest setup, and apply the same change across the other test files
(webhook-handler.test.ts, webhook-validator.test.ts,
twenty-sync-service.test.ts) to ensure a consistent Jest runner configuration.

In
`@packages/twenty-apps/community/meeting-baas-recorder/src/roles/default.role.ts`:
- Around line 15-22: Add an installation README/guide that documents the
required broad read access (canReadAllObjectRecords: true) for this app due to
platform limitations preventing per-object permissions; in the new doc reference
the role flag name canReadAllObjectRecords and list the specific system objects
the code needs read access to (calendarEvent, calendarEventParticipant,
calendarChannelEventAssociation, calendarChannel, connectedAccount,
workspaceMember), explain this is a platform constraint rather than app design,
and note that reads of person, message, messageChannel, and note are not
actually required per the code to address admin concerns.

In
`@packages/twenty-apps/community/meeting-baas-recorder/src/twenty-sync-service.test.ts`:
- Around line 7-11: The axios mock only provides default.get (mocks.axiosGet),
which will break tests if resolveCalendarEventOwner or its module uses axios as
a callable, axios.post, or axios.create; update the vi.mock('axios', ...) to
provide a fuller shape: make the default export callable (a vi.fn()), include
get and post properties (wired to existing mocks like mocks.axiosGet and a new
mocks.axiosPost), and include create that returns an axios-like instance (or a
function that returns the default mock), so functions like
resolveCalendarEventOwner, axios.post, and axios.create resolve to defined mocks
and avoid unclear errors.

In
`@packages/twenty-apps/community/meeting-baas-recorder/src/twenty-sync-service.ts`:
- Around line 155-188: Extract a single helper like getRecordingIdByField(field:
string, value: string): Promise<string | null> that builds the recordings URL
with filter `{ [field]: { eq: value } }` and `limit: 1`, performs the
axios.get<TwentyListResponse<'recordings'>>(url, { headers: authHeaders() }),
and returns the first recording.id or null (centralize the try/catch there).
Then refactor checkIfRecordingExists(botId) to call
getRecordingIdByField('botId', botId) and return its result, and refactor
checkIfRecordingExistsForEvent(calendarEventId) to call
getRecordingIdByField('calendarEventId', calendarEventId) and return a boolean
by checking `!== null`.
- Around line 41-151: The ownershipCache currently stores empty/negative results
unconditionally and is unbounded; update resolveCalendarEventOwner to only write
to ownershipCache when result.workspaceMemberId is present (i.e., a successful
resolution from resolveViaChannelChain or resolveViaParticipants or after
fetching workspaceMemberName), and replace the plain Map with a bounded/LRU
cache (e.g., use the lru-cache package or implement simple max-size eviction) so
entries expire or get evicted to prevent unbounded growth; ensure you keep the
cache lookup/usage via ownershipCache.get(calendarEventId) and the final
ownershipCache.set(calendarEventId, result) semantics but only call set when
result.workspaceMemberId is truthy (and attach a TTL if using LRU/TLS).
- Around line 173-228: The checkIfRecordingExists function swallows all errors
causing upsertRecording to treat transient failures as "no existing record" and
create duplicates; update checkIfRecordingExists to not return null on
unexpected errors but re-throw the caught error (or at least propagate
transient/network errors) so callers like upsertRecording receive the failure
and can retry, and ensure upsertRecording only POSTs when checkIfRecordingExists
resolves to null deterministically; locate the functions checkIfRecordingExists
and upsertRecording in twenty-sync-service.ts and replace the empty catch in
checkIfRecordingExists with code that throws or re-throws the caught error (or
distinguishes and re-throws transient errors) so webhook retries handle
transient failures instead of creating duplicate recordings.

In `@packages/twenty-apps/community/meeting-baas-recorder/src/types.ts`:
- Around line 15-19: Replace the unsafe "as" casts on the WebhookEvent
discriminants with a TypeScript "satisfies" check so the literal values are
validated against the SDK event types at compile time; update the WebhookEvent
constant (the symbol WebhookEvent and its entries currently using
BotWebhookCompleted['event'], BotWebhookFailed['event'],
BotWebhookStatusChange['event']) to use a satisfies constraint ensuring each
string literal matches the corresponding SDK discriminant type rather than
silently asserting via "as".

In `@packages/twenty-apps/community/meeting-baas-recorder/src/utils.ts`:
- Around line 9-12: The restHeaders function currently returns an Authorization
header with an empty bearer when process.env.TWENTY_API_KEY is unset; change
restHeaders to validate process.env.TWENTY_API_KEY and throw a descriptive error
(e.g., "TWENTY_API_KEY is not set") instead of returning an empty Authorization
value so missing configuration fails fast; update the Authorization value to use
the validated key and keep 'Content-Type' as-is.
- Around line 14-38: Update the top doc comment example in buildRestUrl to show
the percent-encoded query string (e.g.
filter=botId%5Beq%5D%3A%22bot-123%22&limit=1) so it matches what URLSearchParams
produces, and make the filter value interpolation defensive by percent-encoding
filter values before embedding them into the filterParts string (use
encodeURIComponent on the `value` inside the mapping that builds
`${field}[${op}]:"${value}"`); reference buildRestUrl, FilterCondition,
params.set and getRestApiUrl when locating the change.

In
`@packages/twenty-apps/community/meeting-baas-recorder/src/views/by-platform.view.ts`:
- Line 2: The import of ViewType currently uses the unavailable bare specifier
'twenty-shared/types'; change it to import the same symbol from the app's local
shim via a relative import to src/shims/twenty-shared/types.ts (update the
import in by-platform.view.ts and the other affected files
all-recordings.view.ts and completed-recordings.view.ts so they import {
ViewType } from the local shim path instead of 'twenty-shared/types').

In
`@packages/twenty-apps/community/meeting-baas-recorder/src/webhook-handler.test.ts`:
- Around line 53-155: Add a new test case in webhook-handler.test.ts for the
'bot.status_change' event that constructs a WebhookHandler and calls handle(...)
with an event payload where event === 'bot.status_change' (authenticated with
'x-mb-secret': 'secret-123'), then assert the returned result indicates
success/handled and that mocks.fetchTranscript, mocks.generateSummary,
mocks.syncBotRecording and mocks.meetingBaasConstructor were not called; also
update the existing "rejects webhooks with mismatched auth headers" and "reports
bot.failed events as errors" tests to additionally assert that
mocks.fetchTranscript, mocks.generateSummary and mocks.meetingBaasConstructor
were not called (besides the existing syncBotRecording assertions) to ensure no
side effects on error paths.

In `@packages/twenty-apps/community/meeting-baas-recorder/src/webhook-handler.ts`:
- Around line 48-57: The bot.failed handler currently throws an Error (in the
payload.event === 'bot.failed' branch) which results in a response body of {
success: false, errors: [...] } while bot.status_change returns { success: true
}, causing asymmetry that can trigger retries if the caller inspects the success
field; change the bot.failed handling in webhook-handler.ts (the payload.event
=== 'bot.failed' branch where failedData is used and this.logger.error is
called) to stop throwing and instead return a 200-style response with success:
true plus an errors array containing the failure message (and still log the
detailed error via this.logger.error) so the service records the failure but
signals no retry.

In
`@packages/twenty-apps/community/meeting-baas-recorder/src/webhook-validator.ts`:
- Around line 120-133: The code unsafely casts wrapper.headers to
Record<string,string>; instead iterate Object.entries(wrapper.headers) and build
extractedHeaders by coercing each value to a string (for a string keep as-is,
for string[] join with ',' or pick first element, for other/undefined set to
undefined or skip) so downstream consumers like verifyWebhookApiKey see a plain
string; update the block that assigns extractedHeaders (and keep variable name
extractedHeaders) to perform this safe coercion before returning payload from
WebhookPayloadSchema.
- Around line 82-86: Replace the simple equality check between mbSecret and
expectedKey with a timing-safe comparison: in the webhook validation code path
(the block using mbSecret and expectedKey in webhook-validator.ts) first compare
lengths and return {isValid: false, reason: 'x-mb-secret mismatch'} if lengths
differ, then convert both strings to Buffers and use crypto.timingSafeEqual to
determine equality, returning {isValid: true} on match and the same mismatch
object otherwise; ensure to import Node's crypto module if not already present.

In
`@packages/twenty-server/src/engine/core-modules/application/application-manifest/application-manifest-migration.service.ts`:
- Around line 331-343: The permissionFlags mapping uses a dead fallback cast
because RoleManifest types permissionFlags as PermissionFlagType[] (strings);
update the code to match the true shape: either (A) if permissionFlags are
always strings, remove the object branch and simplify permissionFlagKeys =
role.permissionFlags (or map to identity) before calling
permissionFlagService.upsertPermissionFlags, or (B) if permissionFlags can be
string|{flag:string}, change the RoleManifest type to (string | { flag: string
})[] and then perform proper narrowing (typeof flag === 'string' ? flag :
flag.flag) without using an `as` cast—apply the corresponding fix around
role.permissionFlags and the call to permissionFlagService.upsertPermissionFlags
to keep types consistent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0eddd2bc-0eff-467b-bca6-e9a89e1747f6

📥 Commits

Reviewing files that changed from the base of the PR and between 9106d8d and 9800390.

⛔ Files ignored due to path filters (1)
  • packages/twenty-apps/community/meeting-baas-recorder/yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (49)
  • packages/twenty-apps/community/meeting-baas-recorder/.env.example
  • packages/twenty-apps/community/meeting-baas-recorder/.gitignore
  • packages/twenty-apps/community/meeting-baas-recorder/application.config.ts
  • packages/twenty-apps/community/meeting-baas-recorder/jest.config.mjs
  • packages/twenty-apps/community/meeting-baas-recorder/package.json
  • packages/twenty-apps/community/meeting-baas-recorder/project.json
  • packages/twenty-apps/community/meeting-baas-recorder/src/__tests__/app-install.integration-test.ts
  • packages/twenty-apps/community/meeting-baas-recorder/src/__tests__/setup-test.ts
  • packages/twenty-apps/community/meeting-baas-recorder/src/application-config.ts
  • packages/twenty-apps/community/meeting-baas-recorder/src/constants/universal-identifiers.ts
  • packages/twenty-apps/community/meeting-baas-recorder/src/fields/bot-entry-message-on-workspace-member.field.ts
  • packages/twenty-apps/community/meeting-baas-recorder/src/fields/bot-name-on-workspace-member.field.ts
  • packages/twenty-apps/community/meeting-baas-recorder/src/fields/calendar-event-on-recording.field.ts
  • packages/twenty-apps/community/meeting-baas-recorder/src/fields/recording-preference-on-workspace-member.field.ts
  • packages/twenty-apps/community/meeting-baas-recorder/src/fields/recordings-on-calendar-event.field.ts
  • packages/twenty-apps/community/meeting-baas-recorder/src/fields/recordings-on-workspace-member.field.ts
  • packages/twenty-apps/community/meeting-baas-recorder/src/fields/workspace-member-on-recording.field.ts
  • packages/twenty-apps/community/meeting-baas-recorder/src/front-components/meeting-baas-settings.front-component.tsx
  • packages/twenty-apps/community/meeting-baas-recorder/src/generate-summary.ts
  • packages/twenty-apps/community/meeting-baas-recorder/src/index.ts
  • packages/twenty-apps/community/meeting-baas-recorder/src/logger.ts
  • packages/twenty-apps/community/meeting-baas-recorder/src/logic-functions/batch-schedule-bots.ts
  • packages/twenty-apps/community/meeting-baas-recorder/src/logic-functions/on-calendar-event-created.ts
  • packages/twenty-apps/community/meeting-baas-recorder/src/logic-functions/on-calendar-event-updated.ts
  • packages/twenty-apps/community/meeting-baas-recorder/src/logic-functions/schedule-bot.ts
  • packages/twenty-apps/community/meeting-baas-recorder/src/meeting-baas-api-client.ts
  • packages/twenty-apps/community/meeting-baas-recorder/src/navigation-menu-items/recordings.navigation-menu-item.ts
  • packages/twenty-apps/community/meeting-baas-recorder/src/objects/index.ts
  • packages/twenty-apps/community/meeting-baas-recorder/src/objects/recording.ts
  • packages/twenty-apps/community/meeting-baas-recorder/src/receive-recording-webhook.test.ts
  • packages/twenty-apps/community/meeting-baas-recorder/src/receive-recording-webhook.ts
  • packages/twenty-apps/community/meeting-baas-recorder/src/roles/default.role.ts
  • packages/twenty-apps/community/meeting-baas-recorder/src/shims/twenty-shared/types.ts
  • packages/twenty-apps/community/meeting-baas-recorder/src/twenty-sync-service.test.ts
  • packages/twenty-apps/community/meeting-baas-recorder/src/twenty-sync-service.ts
  • packages/twenty-apps/community/meeting-baas-recorder/src/types.ts
  • packages/twenty-apps/community/meeting-baas-recorder/src/utils.ts
  • packages/twenty-apps/community/meeting-baas-recorder/src/views/all-recordings.view.ts
  • packages/twenty-apps/community/meeting-baas-recorder/src/views/by-platform.view.ts
  • packages/twenty-apps/community/meeting-baas-recorder/src/views/completed-recordings.view.ts
  • packages/twenty-apps/community/meeting-baas-recorder/src/webhook-handler.test.ts
  • packages/twenty-apps/community/meeting-baas-recorder/src/webhook-handler.ts
  • packages/twenty-apps/community/meeting-baas-recorder/src/webhook-validator.test.ts
  • packages/twenty-apps/community/meeting-baas-recorder/src/webhook-validator.ts
  • packages/twenty-apps/community/meeting-baas-recorder/tsconfig.json
  • packages/twenty-front/src/pages/settings/applications/tabs/SettingsApplicationCustomTab.tsx
  • packages/twenty-server/src/engine/core-modules/application/application-manifest/application-manifest-migration.service.ts
  • packages/twenty-server/src/engine/core-modules/application/application-manifest/converters/from-view-manifest-to-universal-flat-view.util.ts
  • packages/twenty-shared/src/application/viewManifestType.ts

Comment on lines +9 to +12
testMatch: [
'<rootDir>/src/**/__tests__/**/*.(test|spec).{js,ts}',
'<rootDir>/src/**/?(*.)(test|spec).{js,ts}',
],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Test runner does not match the test files' imports.

The test files added in this package (e.g., src/receive-recording-webhook.test.ts) import from vitest, but this config wires up Jest with ts-jest. Jest will fail to resolve vitest at runtime. Please either:

  • Replace this config with a Vitest config (and switch package.json's test script and deps accordingly), or
  • Convert all test files to use Jest's globals and drop vitest imports.

See the paired comment on src/receive-recording-webhook.test.ts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/twenty-apps/community/meeting-baas-recorder/jest.config.mjs` around
lines 9 - 12, The project is configured with Jest (jest.config.mjs) but the new
test files (e.g., src/receive-recording-webhook.test.ts) import from vitest,
causing runtime resolution failures; fix by switching to Vitest: replace
jest.config.mjs with an equivalent Vitest config (vite or vitest config file),
update package.json "test" script to run vitest, and adjust devDependencies to
include vitest (and remove ts-jest/jest if not needed); alternatively, if you
prefer Jest, update src/receive-recording-webhook.test.ts to remove vitest
imports and use Jest globals (convert assertions/mocks to Jest equivalents) and
ensure package.json and devDependencies remain configured for Jest.

Comment on lines +5 to +9
"engines": {
"node": "^24.5.0",
"npm": "please-use-yarn",
"yarn": ">=4.0.2"
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Overly strict Node engine range may break installs in the monorepo.

"node": "^24.5.0" restricts to Node 24.5.x and up within the 24.x major. If the Twenty monorepo root package.json pins a different/lower Node (commonly 18.x or 20.x in Nx/Twenty projects), Yarn will emit EBADENGINE warnings, and CI on LTS Node versions will outright fail the engine check. Please align this with the monorepo-wide Node version (and similarly for Yarn if necessary).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/twenty-apps/community/meeting-baas-recorder/package.json` around
lines 5 - 9, The package.json engines entry is too restrictive ("node":
"^24.5.0") and can cause EBADENGINE failures; update the "engines" field in this
package (the engines object in
packages/twenty-apps/community/meeting-baas-recorder/package.json) to match the
monorepo-wide Node version policy (for example use the same semver range as the
root package.json like "node": ">=18" or the exact range used in the repo) and
adjust the "yarn" entry if the monorepo requires a different Yarn major; ensure
you only change the engines values (the engines object) so local installs and CI
use the monorepo-approved Node/Yarn range.

Comment on lines +28 to +29
const deployResult = await appDeploy({
tarballPath: buildResult.data.tarballPath!,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Non-null assertion on tarballPath.

buildResult.data.tarballPath! relies on the success discriminant guaranteeing the tarball field. If the SDK return shape changes, this will silently pass undefined to deploy. A narrow guard would be safer.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/twenty-apps/community/meeting-baas-recorder/src/__tests__/app-install.integration-test.ts`
around lines 28 - 29, The test uses a non-null assertion on
buildResult.data.tarballPath which can pass undefined into appDeploy; instead
check the build result discriminant and the tarball path explicitly before
calling appDeploy—e.g., assert buildResult.success is true (or throw/fail the
test if not) and verify buildResult.data?.tarballPath is defined, then pass that
verified value to appDeploy (reference: buildResult, buildResult.data,
tarballPath, and appDeploy).

Comment on lines +48 to +56
afterAll(async () => {
const uninstallResult = await appUninstall({ appPath: APP_PATH });

if (!uninstallResult.success) {
console.warn(
`App uninstall failed: ${uninstallResult.error?.message ?? 'Unknown error'}`,
);
}
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Teardown may run uninstall when install never succeeded.

If beforeAll throws after build/deploy but before/during install, afterAll still runs appUninstall, which will attempt to uninstall an app that may not exist. Consider tracking install state and conditionally uninstalling, or ensure appUninstall is idempotent on a no-op.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/twenty-apps/community/meeting-baas-recorder/src/__tests__/app-install.integration-test.ts`
around lines 48 - 56, The teardown calls appUninstall unconditionally in
afterAll which can run even if beforeAll failed; make uninstall conditional by
tracking install success (e.g., a boolean like let installed = false set to true
in the successful install path inside beforeAll or the test setup) and only call
appUninstall when installed is true, or update appUninstall to be
idempotent/no-op on non-existent apps; locate afterAll and the install logic
(beforeAll / install function) and add the install flag check before invoking
appUninstall (or make appUninstall tolerate missing installs).

Comment on lines +12 to +21
const apiUrl = process.env.TWENTY_API_URL!;
const token = process.env.TWENTY_API_KEY!;

if (!apiUrl || !token) {
throw new Error(
'TWENTY_API_URL and TWENTY_API_KEY must be set.\n' +
'Start a local server: yarn twenty server start\n' +
'Or set them in vitest env config.',
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Remove misleading non-null assertions.

apiUrl/token are asserted with ! on lines 12-13, then re-validated on line 15. Either drop the assertions and narrow the types after the check, or drop the check. The current pattern misleads readers and defeats TS safety.

Proposed fix
-  const apiUrl = process.env.TWENTY_API_URL!;
-  const token = process.env.TWENTY_API_KEY!;
-
-  if (!apiUrl || !token) {
+  const apiUrl = process.env.TWENTY_API_URL;
+  const token = process.env.TWENTY_API_KEY;
+
+  if (!apiUrl || !token) {
     throw new Error(
       'TWENTY_API_URL and TWENTY_API_KEY must be set.\n' +
         'Start a local server: yarn twenty server start\n' +
         'Or set them in vitest env config.',
     );
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/twenty-apps/community/meeting-baas-recorder/src/__tests__/setup-test.ts`
around lines 12 - 21, Remove the misleading non-null assertions on apiUrl and
token: don't use process.env.TWENTY_API_URL! or
process.env.TWENTY_API_KEY!—declare them as possibly undefined (const apiUrl =
process.env.TWENTY_API_URL; const token = process.env.TWENTY_API_KEY;) keep the
existing runtime check (if (!apiUrl || !token) throw ...), and then rely on the
narrowed types for apiUrl and token afterwards (or assign them to new consts
like apiUrlVal/tokenVal after the check) so TypeScript knows they are strings
without using !; update any references to use the narrowed variables (apiUrl,
token or the new consts).

@@ -0,0 +1,25 @@
import { defineView } from 'twenty-sdk';
import { ViewType } from 'twenty-shared/types';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 1) Confirm no package or path alias provides 'twenty-shared/types'
fd -t f 'tsconfig*.json' packages/twenty-apps/community/meeting-baas-recorder | xargs -I{} sh -c 'echo "== {} =="; cat {}'
fd -t f 'package.json' packages/twenty-apps/community/meeting-baas-recorder | xargs -I{} sh -c 'echo "== {} =="; cat {}'
# 2) Show all direct imports of twenty-shared across the app
rg -nP "from ['\"]twenty-shared" -g 'packages/twenty-apps/community/meeting-baas-recorder/**'

Repository: Meeting-BaaS/twenty

Length of output: 1873


🏁 Script executed:

#!/bin/bash
# 1) Check if shim file exists
echo "=== Checking shim file ==="
ls -la packages/twenty-apps/community/meeting-baas-recorder/src/shims/twenty-shared/types.ts 2>&1 || echo "Shim file not found"

# 2) Check for bundler configs (webpack, vite, rollup, etc.)
echo -e "\n=== Checking bundler configs ==="
find packages/twenty-apps/community/meeting-baas-recorder -type f \( -name 'webpack*.js' -o -name 'vite.config.*' -o -name 'rollup.config.*' -o -name '.babelrc*' -o -name 'babel.config.*' \) 2>/dev/null

# 3) Check if twenty-sdk exports ViewType
echo -e "\n=== Checking twenty-sdk package for ViewType ==="
find node_modules/twenty-sdk -type f -name '*.d.ts' 2>/dev/null | head -5
rg -l "export.*ViewType" node_modules/twenty-sdk 2>/dev/null || echo "ViewType search in node_modules (may not be available in sandbox)"

# 4) Check entire shims directory structure
echo -e "\n=== Shims directory structure ==="
find packages/twenty-apps/community/meeting-baas-recorder/src/shims -type f 2>/dev/null || echo "No shims directory found"

# 5) Look at how imports are used in other view files
echo -e "\n=== Content of one view file that imports twenty-shared ==="
head -20 packages/twenty-apps/community/meeting-baas-recorder/src/views/completed-recordings.view.ts

Repository: Meeting-BaaS/twenty

Length of output: 1121


🏁 Script executed:

#!/bin/bash
# 1) Check shim file content
echo "=== Shim file content ==="
cat packages/twenty-apps/community/meeting-baas-recorder/src/shims/twenty-shared/types.ts

# 2) Check parent tsconfig files for paths alias
echo -e "\n=== Parent tsconfig.json files ==="
find packages/twenty-apps/community -maxdepth 2 -name 'tsconfig.json' -type f | while read f; do
  echo "=== $f ==="
  cat "$f" | jq '.compilerOptions.paths // "No paths alias"' 2>/dev/null || cat "$f" | grep -A 5 '"paths"' || echo "No paths config"
done

# 3) Check if twenty-sdk exports ViewType
echo -e "\n=== Checking for ViewType in codebase ==="
rg -n "export.*ViewType" packages/twenty-apps/community/meeting-baas-recorder/src/shims/ || echo "Not found in shims"
rg -n "^export.*ViewType" packages/twenty-apps/community/meeting-baas-recorder/src/ | head -5

# 4) Look for tsconfig root
echo -e "\n=== Root tsconfig ==="
ls -la tsconfig.json 2>/dev/null && cat tsconfig.json | jq '.compilerOptions.paths // "No paths"' 2>/dev/null || echo "No root tsconfig"

Repository: Meeting-BaaS/twenty

Length of output: 2371


🏁 Script executed:

#!/bin/bash
# 1) Check build scripts and how the app is meant to be built
echo "=== Build scripts in package.json ==="
cat packages/twenty-apps/community/meeting-baas-recorder/package.json | jq '.scripts' 2>/dev/null

# 2) Check if there's a parent package.json or build config
echo -e "\n=== Parent package.json (packages/twenty-apps/community) ==="
cat packages/twenty-apps/community/package.json 2>/dev/null | head -50 || echo "No parent package.json"

# 3) Check root package.json for any global paths config
echo -e "\n=== Root package.json ==="
cat package.json 2>/dev/null | jq '.workspaces // "No workspaces"' | head -20

# 4) Check if there's a build or dist directory already
echo -e "\n=== Checking for existing build artifacts ==="
ls -la packages/twenty-apps/community/meeting-baas-recorder/dist 2>/dev/null | head -10 || echo "No dist directory"

# 5) Check the actual imports to understand if they would work
echo -e "\n=== Verify the three problem files and their current state ==="
rg -A 5 "from ['\"]twenty-shared" packages/twenty-apps/community/meeting-baas-recorder/src/views/ --no-heading

Repository: Meeting-BaaS/twenty

Length of output: 2942


Fix import paths to use the local shim instead of the unavailable twenty-shared/types.

The imports in this file (and two others: all-recordings.view.ts, completed-recordings.view.ts) cannot resolve twenty-shared/types. Although the app ships a local shim at src/shims/twenty-shared/types.ts that exports ViewType, the code is importing from the bare specifier. There is no tsconfig paths alias or bundler configuration to redirect these imports, and twenty-shared is not in the app's dependencies. This will fail at build/runtime with "Cannot find module 'twenty-shared/types'".

Proposed fix
-import { ViewType } from 'twenty-shared/types';
+import { ViewType } from '../shims/twenty-shared/types';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { ViewType } from 'twenty-shared/types';
import { ViewType } from '../shims/twenty-shared/types';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/twenty-apps/community/meeting-baas-recorder/src/views/by-platform.view.ts`
at line 2, The import of ViewType currently uses the unavailable bare specifier
'twenty-shared/types'; change it to import the same symbol from the app's local
shim via a relative import to src/shims/twenty-shared/types.ts (update the
import in by-platform.view.ts and the other affected files
all-recordings.view.ts and completed-recordings.view.ts so they import {
ViewType } from the local shim path instead of 'twenty-shared/types').

Comment on lines +53 to +155
describe('WebhookHandler', () => {
beforeEach(() => {
vi.clearAllMocks();
process.env.MEETING_BAAS_API_KEY = 'secret-123';

mocks.transformWebhookData.mockReturnValue({
botId: 'bot-123',
title: 'Test Recording',
date: '2026-04-14T10:00:00Z',
duration: 1800,
transcript: '',
mp4Url: 'https://example.com/recording.mp4',
meetingUrl: 'https://meet.google.com/abc-defg-hij',
platform: 'GOOGLE_MEET',
extra: {
calendarEventId: 'calendar-event-1',
workspaceMemberId: 'workspace-member-1',
},
});
mocks.fetchTranscript.mockResolvedValue('Speaker A: Hello\nSpeaker B: Hi');
mocks.generateSummary.mockResolvedValue('Summary text');
mocks.syncBotRecording.mockResolvedValue('recording-123');
});

it('processes a completed webhook authenticated with x-mb-secret', async () => {
const handler = new WebhookHandler();

const result = await handler.handle(
{ body: completedPayload },
{ 'x-mb-secret': 'secret-123' },
);

expect(result).toEqual({
success: true,
errors: [],
durationMinutes: 30,
recordingId: 'recording-123',
});
expect(mocks.meetingBaasConstructor).toHaveBeenCalledWith('secret-123');
expect(mocks.fetchTranscript).toHaveBeenCalledOnce();
expect(mocks.generateSummary).toHaveBeenCalledWith('Speaker A: Hello\nSpeaker B: Hi');
expect(mocks.syncBotRecording).toHaveBeenCalledWith(
expect.objectContaining({
botId: 'bot-123',
title: 'Test Recording',
transcript: 'Speaker A: Hello\nSpeaker B: Hi',
summary: 'Summary text',
calendarEventId: 'calendar-event-1',
workspaceMemberId: 'workspace-member-1',
}),
expect.any(Object),
);
});

it('rejects webhooks with mismatched auth headers', async () => {
const handler = new WebhookHandler();

const result = await handler.handle(completedPayload, {
'x-mb-secret': 'wrong-secret',
});

expect(result).toEqual({
success: false,
errors: ['Invalid webhook API key'],
});
expect(mocks.syncBotRecording).not.toHaveBeenCalled();
});

it('reports bot.failed events as errors', async () => {
const handler = new WebhookHandler();

const result = await handler.handle(
{
event: 'bot.failed',
data: {
bot_id: 'bot-123',
error_message: 'timeout',
error_code: 'TIMEOUT',
},
},
{ 'x-mb-secret': 'secret-123' },
);

expect(result.success).toBe(false);
expect(result.errors).toContain('Meeting BaaS bot failed: timeout');
expect(mocks.syncBotRecording).not.toHaveBeenCalled();
});

it('propagates sync errors to the result', async () => {
mocks.syncBotRecording.mockImplementation((_data: unknown, syncResult: { errors: { botId: string; error: string }[] }) => {
syncResult.errors.push({ botId: 'bot-123', error: '500: Internal Server Error' });
return null;
});

const handler = new WebhookHandler();

const result = await handler.handle(completedPayload, {
'x-mb-secret': 'secret-123',
});

expect(result.success).toBe(false);
expect(result.errors?.[0]).toContain('Failed to sync recording');
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Add a bot.status_change case and assert side-effect absence on error paths.

Two small test-coverage gaps:

  • bot.status_change is part of WebhookPayloadSchema (see webhook-validator.ts) but there is no test here exercising it — worth at least one case asserting it does not call fetchTranscript/generateSummary/syncBotRecording.
  • The auth-mismatch and bot.failed tests assert syncBotRecording is not called, but not that fetchTranscript/generateSummary/meetingBaasConstructor are also skipped — easy to add and prevents regressions where bot failures still trigger a BaaS API round-trip or LLM call.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/twenty-apps/community/meeting-baas-recorder/src/webhook-handler.test.ts`
around lines 53 - 155, Add a new test case in webhook-handler.test.ts for the
'bot.status_change' event that constructs a WebhookHandler and calls handle(...)
with an event payload where event === 'bot.status_change' (authenticated with
'x-mb-secret': 'secret-123'), then assert the returned result indicates
success/handled and that mocks.fetchTranscript, mocks.generateSummary,
mocks.syncBotRecording and mocks.meetingBaasConstructor were not called; also
update the existing "rejects webhooks with mismatched auth headers" and "reports
bot.failed events as errors" tests to additionally assert that
mocks.fetchTranscript, mocks.generateSummary and mocks.meetingBaasConstructor
were not called (besides the existing syncBotRecording assertions) to ensure no
side effects on error paths.

Comment on lines +82 to +86
if (mbSecret) {
return mbSecret === expectedKey
? { isValid: true }
: { isValid: false, reason: 'x-mb-secret mismatch' };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Use a timing-safe comparison for the webhook secret.

mbSecret === expectedKey leaks length- and prefix-dependent timing info. In practice exploitation over the public internet is difficult, but the fix is one line and this is a security-boundary function, so it's worth doing. Make sure to length-check first to avoid timingSafeEqual throwing on mismatched buffer sizes.

🔒 Suggested fix
-import { createHash } from 'crypto';
+import { createHash, timingSafeEqual } from 'crypto';
@@
-  const mbSecret = normalized['x-mb-secret'];
-  if (mbSecret) {
-    return mbSecret === expectedKey
-      ? { isValid: true }
-      : { isValid: false, reason: 'x-mb-secret mismatch' };
-  }
+  const mbSecret = normalized['x-mb-secret'];
+  if (mbSecret) {
+    const a = Buffer.from(mbSecret);
+    const b = Buffer.from(expectedKey);
+    const ok = a.length === b.length && timingSafeEqual(a, b);
+    return ok
+      ? { isValid: true }
+      : { isValid: false, reason: 'x-mb-secret mismatch' };
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (mbSecret) {
return mbSecret === expectedKey
? { isValid: true }
: { isValid: false, reason: 'x-mb-secret mismatch' };
}
const mbSecret = normalized['x-mb-secret'];
if (mbSecret) {
const a = Buffer.from(mbSecret);
const b = Buffer.from(expectedKey);
const ok = a.length === b.length && timingSafeEqual(a, b);
return ok
? { isValid: true }
: { isValid: false, reason: 'x-mb-secret mismatch' };
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/twenty-apps/community/meeting-baas-recorder/src/webhook-validator.ts`
around lines 82 - 86, Replace the simple equality check between mbSecret and
expectedKey with a timing-safe comparison: in the webhook validation code path
(the block using mbSecret and expectedKey in webhook-validator.ts) first compare
lengths and return {isValid: false, reason: 'x-mb-secret mismatch'} if lengths
differ, then convert both strings to Buffers and use crypto.timingSafeEqual to
determine equality, returning {isValid: true} on match and the same mismatch
object otherwise; ensure to import Node's crypto module if not already present.

Comment on lines +120 to +133
if (normalizedParams && typeof normalizedParams === 'object') {
const wrapper = normalizedParams as Record<string, unknown>;

if (wrapper.headers && typeof wrapper.headers === 'object' && !Array.isArray(wrapper.headers)) {
extractedHeaders = wrapper.headers as Record<string, string>;
}

for (const key of ['params', 'payload', 'body', 'data', 'event']) {
const candidate = wrapper[key];
const wrappedResult = WebhookPayloadSchema.safeParse(candidate);
if (wrappedResult.success) {
return { payload: wrappedResult.data, extractedHeaders };
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Header cast is unsound; coerce values to strings.

extractedHeaders = wrapper.headers as Record<string, string> trusts the caller. Node HTTP headers can be string | string[] | undefined, and in webhook wrappers (e.g., some serverless adapters) duplicated headers arrive as arrays. Downstream, verifyWebhookApiKey reads normalized['x-mb-secret'] assuming a plain string, so an array value would silently fail validation with a confusing error.

-    if (wrapper.headers && typeof wrapper.headers === 'object' && !Array.isArray(wrapper.headers)) {
-      extractedHeaders = wrapper.headers as Record<string, string>;
-    }
+    if (wrapper.headers && typeof wrapper.headers === 'object' && !Array.isArray(wrapper.headers)) {
+      extractedHeaders = Object.fromEntries(
+        Object.entries(wrapper.headers as Record<string, unknown>).map(([k, v]) => [
+          k,
+          Array.isArray(v) ? String(v[0] ?? '') : String(v ?? ''),
+        ]),
+      );
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (normalizedParams && typeof normalizedParams === 'object') {
const wrapper = normalizedParams as Record<string, unknown>;
if (wrapper.headers && typeof wrapper.headers === 'object' && !Array.isArray(wrapper.headers)) {
extractedHeaders = wrapper.headers as Record<string, string>;
}
for (const key of ['params', 'payload', 'body', 'data', 'event']) {
const candidate = wrapper[key];
const wrappedResult = WebhookPayloadSchema.safeParse(candidate);
if (wrappedResult.success) {
return { payload: wrappedResult.data, extractedHeaders };
}
}
if (normalizedParams && typeof normalizedParams === 'object') {
const wrapper = normalizedParams as Record<string, unknown>;
if (wrapper.headers && typeof wrapper.headers === 'object' && !Array.isArray(wrapper.headers)) {
extractedHeaders = Object.fromEntries(
Object.entries(wrapper.headers as Record<string, unknown>).map(([k, v]) => [
k,
Array.isArray(v) ? String(v[0] ?? '') : String(v ?? ''),
]),
);
}
for (const key of ['params', 'payload', 'body', 'data', 'event']) {
const candidate = wrapper[key];
const wrappedResult = WebhookPayloadSchema.safeParse(candidate);
if (wrappedResult.success) {
return { payload: wrappedResult.data, extractedHeaders };
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/twenty-apps/community/meeting-baas-recorder/src/webhook-validator.ts`
around lines 120 - 133, The code unsafely casts wrapper.headers to
Record<string,string>; instead iterate Object.entries(wrapper.headers) and build
extractedHeaders by coercing each value to a string (for a string keep as-is,
for string[] join with ',' or pick first element, for other/undefined set to
undefined or skip) so downstream consumers like verifyWebhookApiKey see a plain
string; update the block that assigns extractedHeaders (and keep variable name
extractedHeaders) to perform this safe coercion before returning payload from
WebhookPayloadSchema.

Comment on lines 331 to 343
if (isDefined(role.permissionFlags) && role.permissionFlags.length > 0) {
const permissionFlagKeys = role.permissionFlags.map((flag) =>
typeof flag === 'string' ? flag : (flag as { flag: string }).flag,
);

await this.permissionFlagService.upsertPermissionFlags({
workspaceId,
input: {
roleId,
permissionFlagKeys: role.permissionFlags,
permissionFlagKeys,
},
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect the RoleManifest.permissionFlags type definition in twenty-shared.
fd -t f 'role.*manifest' -i | head -50
rg -nP -C2 '\bpermissionFlags\??\s*:' --type=ts

Repository: Meeting-BaaS/twenty

Length of output: 49428


🏁 Script executed:

#!/bin/bash
# View the method containing lines 331-343 and surrounding context
cat -n packages/twenty-server/src/engine/core-modules/application/application-manifest/application-manifest-migration.service.ts | sed -n '320,355p'

Repository: Meeting-BaaS/twenty

Length of output: 933


🏁 Script executed:

#!/bin/bash
# View method signature and broader context to understand what `role` is
cat -n packages/twenty-server/src/engine/core-modules/application/application-manifest/application-manifest-migration.service.ts | sed -n '270,345p'

Repository: Meeting-BaaS/twenty

Length of output: 2991


🏁 Script executed:

#!/bin/bash
# Find the method signature containing this code block
cat -n packages/twenty-server/src/engine/core-modules/application/application-manifest/application-manifest-migration.service.ts | sed -n '200,280p'

Repository: Meeting-BaaS/twenty

Length of output: 3335


🏁 Script executed:

#!/bin/bash
# Check the PermissionFlagType definition
cat -n packages/twenty-shared/src/application/roleManifestType.ts | head -40

Repository: Meeting-BaaS/twenty

Length of output: 1442


🏁 Script executed:

#!/bin/bash
# Check what PermissionFlagType is defined as
fd -t f 'constants' packages/twenty-shared/src | xargs grep -l 'PermissionFlagType' | head -5

Repository: Meeting-BaaS/twenty

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Search for PermissionFlagType definition more broadly
rg -t ts 'enum.*PermissionFlagType|type.*PermissionFlagType.*=' --max-count=5 packages/twenty-shared/src/

Repository: Meeting-BaaS/twenty

Length of output: 154


🏁 Script executed:

#!/bin/bash
# View the PermissionFlagType enum definition
cat packages/twenty-shared/src/constants/PermissionFlagType.ts

Repository: Meeting-BaaS/twenty

Length of output: 959


🏁 Script executed:

#!/bin/bash
# Check if there are any usages or test cases showing permissionFlags with object structure
rg -A5 -B5 'permissionFlags.*\{.*flag' --type=ts packages/twenty-server packages/twenty-shared packages/twenty-sdk | head -60

Repository: Meeting-BaaS/twenty

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Look for manifest examples or fixtures that might show the permissionFlags structure
fd -t f 'manifest' -i packages/twenty-sdk/src/cli/__tests__/apps | head -20

Repository: Meeting-BaaS/twenty

Length of output: 562


🏁 Script executed:

#!/bin/bash
# Check the expected manifest structure
cat packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__integration__/app-dev/expected-manifest.ts | grep -A10 -B5 'permissionFlags'

Repository: Meeting-BaaS/twenty

Length of output: 547


Avoid as cast; fix the type or remove dead code.

The code assumes role.permissionFlags might contain objects with a flag property, but RoleManifest types permissionFlags strictly as PermissionFlagType[] (string enum). The typeof flag === 'string' check will always be true, making the fallback (flag as { flag: string }).flag dead code. Either:

  1. If permissionFlags is always string[] — remove the object branch entirely:

    const permissionFlagKeys = role.permissionFlags;
  2. If permissionFlags can be a union — update the RoleManifest type to (string | { flag: string })[] and use proper narrowing without as:

    const permissionFlagKeys = role.permissionFlags.map((flag) => {
      if (typeof flag === 'string') return flag;
      if (isDefined(flag) && typeof flag === 'object' && 'flag' in flag) {
        return flag.flag;
      }
      throw new ApplicationException(
        `Invalid permission flag for role ${role.universalIdentifier}`,
        ApplicationExceptionCode.INVALID_REQUEST,
      );
    });

Confirm the upstream manifest schema to determine which applies.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/twenty-server/src/engine/core-modules/application/application-manifest/application-manifest-migration.service.ts`
around lines 331 - 343, The permissionFlags mapping uses a dead fallback cast
because RoleManifest types permissionFlags as PermissionFlagType[] (strings);
update the code to match the true shape: either (A) if permissionFlags are
always strings, remove the object branch and simplify permissionFlagKeys =
role.permissionFlags (or map to identity) before calling
permissionFlagService.upsertPermissionFlags, or (B) if permissionFlags can be
string|{flag:string}, change the RoleManifest type to (string | { flag: string
})[] and then perform proper narrowing (typeof flag === 'string' ? flag :
flag.flag) without using an `as` cast—apply the corresponding fix around
role.permissionFlags and the call to permissionFlagService.upsertPermissionFlags
to keep types consistent.

@coderabbitai coderabbitai 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.

Review continued from previous batch...

Comment on lines +230 to +260
try {
const { botIds, errors } = await client.batchCreateScheduledBots(items);

// Create placeholder recordings for successfully scheduled bots
for (let j = 0; j < botIds.length; j++) {
const event = batch[j];
try {
await upsertRecording({
botId: botIds[j],
name: event.title ? `Scheduled: ${event.title}` : `Scheduled: ${event.conferenceUrl}`,
date: event.startsAt!,
duration: 0,
platform: detectPlatform(event.conferenceUrl!),
status: 'IN_PROGRESS',
meetingUrl: { primaryLinkLabel: 'Join Meeting', primaryLinkUrl: event.conferenceUrl!, secondaryLinks: null },
mp4Url: null,
transcript: '',
calendarEventId: event.id,
workspaceMemberId: event.workspaceMemberId,
});
} catch (error) {
const msg = error instanceof Error ? error.message : String(error);
logger.warn(`Failed to create placeholder for bot ${botIds[j]}: ${msg}`);
}
result.scheduled++;
}

for (const err of errors) {
const event = batch[err.index];
result.errors.push(`Event ${event?.id}: ${err.code} - ${err.message}`);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

Meeting BaaS SDK batchCreateScheduledBots response format: does result.data array correspond to successful items in input order, or does it include null entries for failed items?

💡 Result:

The result.data array in the Meeting BaaS batchCreateScheduledBots response (corresponding to POST /v2/bots/scheduled/batch) contains only the successful items. It does not include null entries for failed items. Failed items are instead listed separately in the response's errors array, where each error includes an index corresponding to its position in the input array (0-based). This follows the same pattern as the regular batchCreateBots endpoint (POST /v2/bots/batch), which is explicitly documented to return partial success responses with data[] for successes and errors[] for failures. Official documentation confirms batch operations process items sequentially by index, with independent validation, and always return success: true even with partial or total failures—requiring checks of both arrays. The SDK (@meeting-baas/sdk) handles batch routes identically, returning { success: true, data: [...], errors: [...] }. Example response structure from docs: { "success": true, "data": [ { "index": 0, "bot_id": "uuid", "extra": {...} } ], "errors": [ { "index": 1, "code": "error_code", "message": "error message", "details": {...}, "extra": {...} } ] } To match results to input order, use the index fields in both data and errors arrays.

Citations:


Index misalignment between botIds and batch due to sparse response mapping.

The loop at line 234 assumes botIds[j] corresponds to batch[j], but the Meeting BaaS SDK returns result.data containing only successful items (not aligned to input indices). When any batch item fails, result.data is shorter than batch, and positional indices no longer align.

Example: If batch = [A, B, C] and item B fails:

  • result.data = [{ bot_id: "uuid1", index: 0 }, { bot_id: "uuid2", index: 2 }]
  • botIds = ["uuid1", "uuid2"]
  • Loop iteration j=1: batch[1] (item B) gets matched with botIds[1] (uuid2, which belongs to batch[2])

This causes placeholder recordings to be linked to the wrong calendar events and workspace members, corrupting the Recording↔CalendarEvent relationship.

Fix: Use the index field returned by the SDK to map results back to batch items:

-        const { botIds, errors } = await client.batchCreateScheduledBots(items);
-        for (let j = 0; j < botIds.length; j++) {
-          const event = batch[j];
+        const { data, errors } = await client.batchCreateScheduledBots(items);
+        for (const result of data) {
+          const event = batch[result.index];
           try {
             await upsertRecording({
-              botId: botIds[j],
+              botId: result.bot_id,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/twenty-apps/community/meeting-baas-recorder/src/logic-functions/batch-schedule-bots.ts`
around lines 230 - 260, The loop assumes positional alignment between botIds and
batch but the SDK returns successful items with their original index, causing
mis-mapping; update the code that handles the batchCreateScheduledBots response
(the call to client.batchCreateScheduledBots and the subsequent
placeholder-creation loop) to iterate over the SDK's successful result entries
(use the returned index field for each successful item) to look up the correct
event from batch[index] and use that entry's workspaceMemberId/calendarEventId
when calling upsertRecording (instead of using botIds[j] with batch[j]); also
increment result.scheduled only for each successfully-mapped item and retain the
existing error mapping using errors[].index.

Comment on lines +48 to +57
if (payload.event === 'bot.failed') {
const failedData = payload.data;
this.logger.error(`bot failed: ${failedData.error_message} (${failedData.error_code})`);
throw new Error(`Meeting BaaS bot failed: ${failedData.error_message}`);
}

if (payload.event === 'bot.status_change') {
this.logger.debug(`bot status change: ${JSON.stringify(payload.data)}`);
return { success: true };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect how receive-recording-webhook.ts maps handler result to HTTP status
fd -t f 'receive-recording-webhook' --exec cat {}

Repository: Meeting-BaaS/twenty

Length of output: 1398


🏁 Script executed:

# Find and inspect webhook-handler.ts to see the complete flow
fd -t f 'webhook-handler.ts' -x head -n 150 {}

Repository: Meeting-BaaS/twenty

Length of output: 11211


🏁 Script executed:

# Look for ProcessResult type definition and how it's used
fd -t f 'types.ts' -path '*/meeting-baas-recorder/*' -x cat {}

Repository: Meeting-BaaS/twenty

Length of output: 231


🏁 Script executed:

# Search for how defineLogicFunction or the SDK handles ProcessResult mapping to HTTP
fd -t f -e ts -e js | head -20 | xargs grep -l "ProcessResult\|defineLogicFunction" | head -5

Repository: Meeting-BaaS/twenty

Length of output: 45


🏁 Script executed:

# Find the types.ts file in meeting-baas-recorder
fd -t f 'types.ts' --path '*/meeting-baas-recorder/*' -x cat {}

Repository: Meeting-BaaS/twenty

Length of output: 296


🏁 Script executed:

# Search for how ProcessResult is used or if there's any HTTP status mapping
rg "ProcessResult|defineLogicFunction" --max-count 20 -A 3

Repository: Meeting-BaaS/twenty

Length of output: 50378


🏁 Script executed:

# Find types.ts file in meeting-baas-recorder more carefully
find . -name "types.ts" | grep meeting-baas

Repository: Meeting-BaaS/twenty

Length of output: 218


🏁 Script executed:

# Get the define-logic-function implementation
cat packages/twenty-sdk/src/sdk/logic-functions/define-logic-function.ts

Repository: Meeting-BaaS/twenty

Length of output: 1403


🏁 Script executed:

# Get ProcessResult type definition from meeting-baas-recorder
cat ./packages/twenty-apps/community/meeting-baas-recorder/src/types.ts

Repository: Meeting-BaaS/twenty

Length of output: 2222


🏁 Script executed:

# Search for how HTTP route handlers map success to status code in the SDK
rg "success.*200|200.*success|statusCode|status.*code" packages/twenty-sdk/src --max-count 15 -B 2 -A 2

Repository: Meeting-BaaS/twenty

Length of output: 622


🏁 Script executed:

# Search for HTTP response handling in the SDK or executor
rg "statusCode|status.*code|handler.*http|http.*response" packages/twenty-sdk/src/sdk/logic-functions -B 3 -A 3 --max-count 20

Repository: Meeting-BaaS/twenty

Length of output: 45


🏁 Script executed:

# Try to find the executor that actually runs logic functions and handles responses
find packages/twenty-sdk -name "*executor*" -o -name "*runner*" | head -10

Repository: Meeting-BaaS/twenty

Length of output: 45


🏁 Script executed:

# Search for httpRouteTriggerSettings usage to find where responses are handled
rg "httpRouteTriggerSettings" packages/twenty-sdk -B 3 -A 10 --max-count 10

Repository: Meeting-BaaS/twenty

Length of output: 29254


🏁 Script executed:

# Look for where logic functions are actually executed/invoked
rg "handler\(" packages/twenty-sdk/src/sdk/logic-functions --max-count 5 -B 2 -A 5

Repository: Meeting-BaaS/twenty

Length of output: 45


🏁 Script executed:

# Search for where logic function handlers are actually executed and responses handled
rg "handler\(" packages --max-count 20 -A 5 | head -60

Repository: Meeting-BaaS/twenty

Length of output: 4379


🏁 Script executed:

# Look for HTTP executor or response building in packages/twenty-server
find packages/twenty-server -name "*executor*" -o -name "*handler*" -o -name "*http*" | head -20

Repository: Meeting-BaaS/twenty

Length of output: 2330


🏁 Script executed:

# Search for logic function execution in twenty-server
find packages/twenty-server -name "*logic*function*" -type f | head -20

Repository: Meeting-BaaS/twenty

Length of output: 2543


🏁 Script executed:

# Look for route handler that executes logic functions
rg "logicFunction|logic-function" packages/twenty-server/src/engine/api --max-count 15 -l

Repository: Meeting-BaaS/twenty

Length of output: 265


🏁 Script executed:

# Read the logic function executor service
cat packages/twenty-server/src/engine/core-modules/logic-function/logic-function-executor/logic-function-executor.service.ts | head -150

Repository: Meeting-BaaS/twenty

Length of output: 5718


🏁 Script executed:

# Look for route trigger handling
cat packages/twenty-server/src/engine/core-modules/logic-function/logic-function-trigger/triggers/route/utils/build-logic-function-event.util.ts

Repository: Meeting-BaaS/twenty

Length of output: 4091


🏁 Script executed:

# Search for the route handler that uses buildLogicFunctionEvent
rg "buildLogicFunctionEvent" packages/twenty-server -B 5 -A 15 | head -100

Repository: Meeting-BaaS/twenty

Length of output: 16799


🏁 Script executed:

# Search for route handler that calls logic function executor and returns response
rg "LogicFunctionExecutorService.*execute|execute.*logicFunction" packages/twenty-server -l | head -10

Repository: Meeting-BaaS/twenty

Length of output: 164


🏁 Script executed:

# Read the full route-trigger.service.ts to see how result is returned to HTTP response
cat packages/twenty-server/src/engine/core-modules/logic-function/logic-function-trigger/triggers/route/route-trigger.service.ts

Repository: Meeting-BaaS/twenty

Length of output: 5056


bot.failed returns success: false while bot.status_change returns success: true — confirm whether this asymmetry is intentional.

bot.failed throws (caught at line 123) and yields { success: false, errors: [...] } with HTTP 200; bot.status_change returns { success: true } early with HTTP 200. Both result in HTTP 200 responses. If Meeting BaaS's retry logic inspects the success field in the response body (rather than HTTP status), a failed bot event will trigger retries indefinitely despite being a terminal error. Consider returning { success: true, errors: [failureMessage] } for bot.failed to prevent unwanted retries while still recording the failure.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/twenty-apps/community/meeting-baas-recorder/src/webhook-handler.ts`
around lines 48 - 57, The bot.failed handler currently throws an Error (in the
payload.event === 'bot.failed' branch) which results in a response body of {
success: false, errors: [...] } while bot.status_change returns { success: true
}, causing asymmetry that can trigger retries if the caller inspects the success
field; change the bot.failed handling in webhook-handler.ts (the payload.event
=== 'bot.failed' branch where failedData is used and this.logger.error is
called) to stop throwing and instead return a 200-style response with success:
true plus an errors array containing the failure message (and still log the
detailed error via this.logger.error) so the service records the failure but
signals no retry.

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.

2 participants