@twentyhq/call-recorder 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. package/manifest.json +297 -0
  2. package/package.json +2 -2
  3. package/src/front-components/calendar-event-recording.front-component.mjs +269 -0
  4. package/src/front-components/calendar-event-recording.front-component.mjs.map +7 -0
  5. package/src/logic-functions/process-recall-webhook.mjs +1744 -0
  6. package/src/logic-functions/process-recall-webhook.mjs.map +7 -0
  7. package/src/logic-functions/recall-webhook.mjs +391 -0
  8. package/src/logic-functions/recall-webhook.mjs.map +7 -0
  9. package/src/logic-functions/reconcile-call-recorder-calendar-event.mjs +1602 -0
  10. package/src/logic-functions/reconcile-call-recorder-calendar-event.mjs.map +7 -0
  11. package/src/logic-functions/reconcile-stale-bot-state.mjs +2268 -0
  12. package/src/logic-functions/reconcile-stale-bot-state.mjs.map +7 -0
  13. package/.env.example +0 -5
  14. package/.nvmrc +0 -1
  15. package/.oxlintrc.json +0 -20
  16. package/AGENTS.md +0 -67
  17. package/CLAUDE.md +0 -67
  18. package/README.md +0 -24
  19. package/SETUP.md +0 -95
  20. package/src/__tests__/global-setup.ts +0 -100
  21. package/src/__tests__/schema.integration-test.ts +0 -104
  22. package/src/application-config.ts +0 -96
  23. package/src/constants/__tests__/call-recording-field-universal-identifiers.test.ts +0 -19
  24. package/src/constants/app-description.ts +0 -2
  25. package/src/constants/app-display-name.ts +0 -1
  26. package/src/constants/application-universal-identifier.ts +0 -2
  27. package/src/constants/calendar-event-reconciliation-logic-function-universal-identifier.ts +0 -2
  28. package/src/constants/calendar-event-record-page-layout-universal-identifier.ts +0 -2
  29. package/src/constants/calendar-event-recording-front-component-universal-identifier.ts +0 -2
  30. package/src/constants/calendar-event-recording-page-layout-tab-universal-identifier.ts +0 -2
  31. package/src/constants/calendar-event-recording-page-layout-widget-universal-identifier.ts +0 -2
  32. package/src/constants/call-recorder-everyone-left-timeout-seconds-app-variable-universal-identifier.ts +0 -2
  33. package/src/constants/call-recorder-failure-reason-on-call-recording-field-universal-identifier.ts +0 -2
  34. package/src/constants/call-recorder-join-early-minutes-app-variable-universal-identifier.ts +0 -2
  35. package/src/constants/call-recorder-name-app-variable-universal-identifier.ts +0 -2
  36. package/src/constants/call-recorder-noone-joined-timeout-seconds-app-variable-universal-identifier.ts +0 -2
  37. package/src/constants/call-recorder-preference-off-option-id.ts +0 -2
  38. package/src/constants/call-recorder-preference-on-calendar-event-field-universal-identifier.ts +0 -2
  39. package/src/constants/call-recorder-preference-on-calendar-event-view-field-universal-identifier.ts +0 -2
  40. package/src/constants/call-recorder-preference-on-option-id.ts +0 -2
  41. package/src/constants/call-recorder-preference.ts +0 -4
  42. package/src/constants/call-recorder-waiting-room-timeout-seconds-app-variable-universal-identifier.ts +0 -2
  43. package/src/constants/call-recording-audio-field-universal-identifier.ts +0 -2
  44. package/src/constants/call-recording-video-field-universal-identifier.ts +0 -2
  45. package/src/constants/default-role-universal-identifier.ts +0 -2
  46. package/src/constants/process-recall-webhook-logic-function-universal-identifier.ts +0 -2
  47. package/src/constants/recall-webhook-logic-function-universal-identifier.ts +0 -2
  48. package/src/constants/stale-bot-state-logic-function-universal-identifier.ts +0 -2
  49. package/src/default-role.ts +0 -69
  50. package/src/fields/call-recorder-failure-reason-on-call-recording.field.ts +0 -22
  51. package/src/fields/call-recorder-preference-on-calendar-event.field.ts +0 -41
  52. package/src/front-components/calendar-event-recording.front-component.tsx +0 -13
  53. package/src/front-components/components/CalendarEventRecording.tsx +0 -39
  54. package/src/front-components/components/CalendarEventRecordingBody.tsx +0 -96
  55. package/src/front-components/components/CalendarEventRecordingContent.tsx +0 -111
  56. package/src/front-components/components/RecordingTranscript.tsx +0 -92
  57. package/src/front-components/components/RecordingVideoPlayer.tsx +0 -52
  58. package/src/front-components/components/TranscriptEntryList.tsx +0 -61
  59. package/src/front-components/components/TranscriptEntryListItem.tsx +0 -115
  60. package/src/front-components/components/TranscriptErrorBox.tsx +0 -48
  61. package/src/front-components/components/TranscriptSpeakerAvatar.tsx +0 -141
  62. package/src/front-components/components/TranscriptSpeakerChip.tsx +0 -51
  63. package/src/front-components/constants/recording-theme-css-variables.ts +0 -40
  64. package/src/front-components/hooks/use-calendar-event-participants.ts +0 -172
  65. package/src/front-components/hooks/use-calendar-event-recording.ts +0 -155
  66. package/src/front-components/types/calendar-event-participant-by-speaker-name.type.ts +0 -6
  67. package/src/front-components/types/calendar-event-recording-participant.type.ts +0 -7
  68. package/src/front-components/types/transcript-entry.type.ts +0 -13
  69. package/src/front-components/utils/__tests__/find-active-transcript-entry-index.test.ts +0 -66
  70. package/src/front-components/utils/__tests__/format-transcript-timestamp.test.ts +0 -29
  71. package/src/front-components/utils/__tests__/get-speaker-name-match-keys.test.ts +0 -22
  72. package/src/front-components/utils/__tests__/parse-transcript-entries.test.ts +0 -162
  73. package/src/front-components/utils/build-calendar-event-participant-by-speaker-name.util.ts +0 -45
  74. package/src/front-components/utils/find-active-transcript-entry-index.util.ts +0 -77
  75. package/src/front-components/utils/format-transcript-timestamp.util.ts +0 -16
  76. package/src/front-components/utils/get-absolute-avatar-url.util.ts +0 -48
  77. package/src/front-components/utils/get-calendar-event-participant-for-speaker-name.util.ts +0 -24
  78. package/src/front-components/utils/get-speaker-name-match-keys.util.ts +0 -64
  79. package/src/front-components/utils/get-video-file-extension.util.ts +0 -23
  80. package/src/front-components/utils/parse-transcript-entries.util.ts +0 -85
  81. package/src/logic-functions/__tests__/process-recall-webhook.test.ts +0 -62
  82. package/src/logic-functions/__tests__/recall-webhook.test.ts +0 -180
  83. package/src/logic-functions/constants/call-recorder-everyone-left-timeout-seconds-env-var-name.ts +0 -2
  84. package/src/logic-functions/constants/call-recorder-everyone-left-timeout-seconds.ts +0 -1
  85. package/src/logic-functions/constants/call-recorder-join-early-minutes-env-var-name.ts +0 -2
  86. package/src/logic-functions/constants/call-recorder-name-env-var-name.ts +0 -1
  87. package/src/logic-functions/constants/call-recorder-noone-joined-timeout-seconds-env-var-name.ts +0 -2
  88. package/src/logic-functions/constants/call-recorder-noone-joined-timeout-seconds.ts +0 -1
  89. package/src/logic-functions/constants/call-recorder-recording-retention-hours-env-var-name.ts +0 -2
  90. package/src/logic-functions/constants/call-recorder-waiting-room-timeout-seconds-env-var-name.ts +0 -2
  91. package/src/logic-functions/constants/call-recorder-waiting-room-timeout-seconds.ts +0 -1
  92. package/src/logic-functions/constants/call-recording-micro-credits-per-hour.ts +0 -1
  93. package/src/logic-functions/constants/call-recording-request-status.ts +0 -5
  94. package/src/logic-functions/constants/call-recording-status.ts +0 -9
  95. package/src/logic-functions/constants/default-call-recorder-join-early-minutes.ts +0 -1
  96. package/src/logic-functions/constants/default-call-recorder-name.ts +0 -1
  97. package/src/logic-functions/constants/default-call-recorder-recording-retention-hours.ts +0 -2
  98. package/src/logic-functions/constants/default-recall-region.ts +0 -1
  99. package/src/logic-functions/constants/milliseconds-per-minute.ts +0 -1
  100. package/src/logic-functions/constants/non-terminal-call-recording-statuses.ts +0 -8
  101. package/src/logic-functions/constants/recall-api-key-env-var-name.ts +0 -1
  102. package/src/logic-functions/constants/recall-api-max-attempts.ts +0 -1
  103. package/src/logic-functions/constants/recall-api-retry-delay-ms.ts +0 -1
  104. package/src/logic-functions/constants/recall-bot-automatic-leave.ts +0 -74
  105. package/src/logic-functions/constants/recall-bot-everyone-left-min-activate-after-seconds.ts +0 -1
  106. package/src/logic-functions/constants/recall-bot-recording-config.ts +0 -34
  107. package/src/logic-functions/constants/recall-region-env-var-name.ts +0 -1
  108. package/src/logic-functions/constants/recall-webhook-secret-env-var-name.ts +0 -1
  109. package/src/logic-functions/constants/restricted-field-placeholder.ts +0 -3
  110. package/src/logic-functions/constants/stale-bot-state-cron-pattern.ts +0 -1
  111. package/src/logic-functions/constants/twenty-page-size.ts +0 -1
  112. package/src/logic-functions/data/__tests__/complete-call-recording-ingestion.test.ts +0 -55
  113. package/src/logic-functions/data/__tests__/fetch-all-nodes.test.ts +0 -43
  114. package/src/logic-functions/data/__tests__/get-current-workspace-id.test.ts +0 -38
  115. package/src/logic-functions/data/__tests__/strip-restricted-field-value.test.ts +0 -22
  116. package/src/logic-functions/data/complete-call-recording-ingestion.util.ts +0 -24
  117. package/src/logic-functions/data/create-call-recording.util.ts +0 -41
  118. package/src/logic-functions/data/fetch-all-nodes.util.ts +0 -44
  119. package/src/logic-functions/data/fetch-calendar-events-by-filter.util.ts +0 -80
  120. package/src/logic-functions/data/fetch-calendar-events-by-ids.util.ts +0 -20
  121. package/src/logic-functions/data/fetch-calendar-events-by-starts-at-values.util.ts +0 -19
  122. package/src/logic-functions/data/find-call-recordings-by-calendar-event-ids.util.ts +0 -17
  123. package/src/logic-functions/data/find-call-recordings-by-filter.util.ts +0 -102
  124. package/src/logic-functions/data/find-call-recordings-by-ids.util.ts +0 -17
  125. package/src/logic-functions/data/find-open-scheduled-call-recordings.util.ts +0 -14
  126. package/src/logic-functions/data/get-current-workspace-id.util.ts +0 -36
  127. package/src/logic-functions/data/strip-restricted-field-value.util.ts +0 -6
  128. package/src/logic-functions/data/update-call-recording.util.ts +0 -24
  129. package/src/logic-functions/domain/__tests__/build-call-recorder-policy-result.test.ts +0 -47
  130. package/src/logic-functions/domain/__tests__/compute-call-recording-charge.test.ts +0 -71
  131. package/src/logic-functions/domain/__tests__/compute-call-recording-id-for-meeting.test.ts +0 -37
  132. package/src/logic-functions/domain/__tests__/compute-real-meeting-key.test.ts +0 -88
  133. package/src/logic-functions/domain/__tests__/is-call-recording-ingestion-complete.test.ts +0 -59
  134. package/src/logic-functions/domain/__tests__/is-call-recording-status-downgrade.test.ts +0 -37
  135. package/src/logic-functions/domain/__tests__/resolve-call-recorder-policy-result.test.ts +0 -120
  136. package/src/logic-functions/domain/__tests__/should-complete-call-recording-ingestion.test.ts +0 -102
  137. package/src/logic-functions/domain/aggregate-call-recorder-policy-results-by-meeting.util.ts +0 -42
  138. package/src/logic-functions/domain/build-call-recorder-policy-result.util.ts +0 -53
  139. package/src/logic-functions/domain/build-failed-transcript-marker.util.ts +0 -13
  140. package/src/logic-functions/domain/build-pending-transcript-marker.util.ts +0 -13
  141. package/src/logic-functions/domain/build-recall-routing-metadata.util.ts +0 -12
  142. package/src/logic-functions/domain/build-transcript-failure-reason.util.ts +0 -7
  143. package/src/logic-functions/domain/compute-call-recording-charge.util.ts +0 -41
  144. package/src/logic-functions/domain/compute-call-recording-id-for-meeting.util.ts +0 -16
  145. package/src/logic-functions/domain/compute-real-meeting-key.util.ts +0 -48
  146. package/src/logic-functions/domain/compute-recall-bot-join-at.util.ts +0 -34
  147. package/src/logic-functions/domain/is-call-recording-ingestion-complete.util.ts +0 -19
  148. package/src/logic-functions/domain/is-call-recording-status-downgrade.util.ts +0 -37
  149. package/src/logic-functions/domain/is-recall-recording-done-signal.util.ts +0 -13
  150. package/src/logic-functions/domain/map-recall-status-code-to-call-recording-status.util.ts +0 -26
  151. package/src/logic-functions/domain/parse-transcript-marker.util.ts +0 -29
  152. package/src/logic-functions/domain/resolve-call-recorder-policy-result.util.ts +0 -72
  153. package/src/logic-functions/domain/should-complete-call-recording-ingestion.util.ts +0 -32
  154. package/src/logic-functions/flows/__tests__/charge-completed-call-recording.test.ts +0 -45
  155. package/src/logic-functions/flows/__tests__/complete-and-charge-call-recording.test.ts +0 -61
  156. package/src/logic-functions/flows/__tests__/converge-diverged-call-recordings.test.ts +0 -727
  157. package/src/logic-functions/flows/__tests__/download-transcript.test.ts +0 -74
  158. package/src/logic-functions/flows/__tests__/handle-recall-webhook.test.ts +0 -1301
  159. package/src/logic-functions/flows/__tests__/heal-call-recordings-missing-bot.test.ts +0 -225
  160. package/src/logic-functions/flows/__tests__/ingest-call-recording-media.test.ts +0 -153
  161. package/src/logic-functions/flows/__tests__/reap-orphaned-call-recorders.test.ts +0 -425
  162. package/src/logic-functions/flows/__tests__/reconcile-call-recorder.test.ts +0 -1007
  163. package/src/logic-functions/flows/cancel-call-recording-request.util.ts +0 -46
  164. package/src/logic-functions/flows/charge-completed-call-recording.util.ts +0 -31
  165. package/src/logic-functions/flows/complete-and-charge-call-recording.util.ts +0 -29
  166. package/src/logic-functions/flows/converge-diverged-call-recordings-result.type.ts +0 -8
  167. package/src/logic-functions/flows/converge-diverged-call-recordings.util.ts +0 -447
  168. package/src/logic-functions/flows/download-transcript.util.ts +0 -67
  169. package/src/logic-functions/flows/ensure-call-recorder.util.ts +0 -73
  170. package/src/logic-functions/flows/handle-recall-webhook.util.ts +0 -672
  171. package/src/logic-functions/flows/heal-call-recordings-missing-bot.util.ts +0 -82
  172. package/src/logic-functions/flows/ingest-call-recording-media.util.ts +0 -128
  173. package/src/logic-functions/flows/persist-call-recording-progress.util.ts +0 -58
  174. package/src/logic-functions/flows/reap-orphaned-call-recorders.util.ts +0 -183
  175. package/src/logic-functions/flows/reconcile-call-recorder.util.ts +0 -495
  176. package/src/logic-functions/flows/reconcile-call-recording-transcript-artifact-result.type.ts +0 -11
  177. package/src/logic-functions/flows/reconcile-call-recording-transcript-artifact.util.ts +0 -182
  178. package/src/logic-functions/flows/reschedule-call-recording-bot.util.ts +0 -69
  179. package/src/logic-functions/process-recall-webhook.ts +0 -23
  180. package/src/logic-functions/recall-api/__tests__/extract-recall-bot-convergence.test.ts +0 -153
  181. package/src/logic-functions/recall-api/__tests__/extract-recall-media-urls.test.ts +0 -67
  182. package/src/logic-functions/recall-api/__tests__/recall-bot-api.test.ts +0 -744
  183. package/src/logic-functions/recall-api/__tests__/verify-recall-webhook-signature.test.ts +0 -122
  184. package/src/logic-functions/recall-api/cancel-recall-bot.util.ts +0 -28
  185. package/src/logic-functions/recall-api/create-async-recall-transcript.util.ts +0 -47
  186. package/src/logic-functions/recall-api/eject-recall-bot.util.ts +0 -28
  187. package/src/logic-functions/recall-api/extract-recall-bot-convergence.util.ts +0 -149
  188. package/src/logic-functions/recall-api/extract-recall-bot-id.util.ts +0 -10
  189. package/src/logic-functions/recall-api/extract-recall-media-urls.util.ts +0 -30
  190. package/src/logic-functions/recall-api/extract-twenty-workspace-id-from-recall-webhook.util.ts +0 -8
  191. package/src/logic-functions/recall-api/get-recall-api-config.util.ts +0 -59
  192. package/src/logic-functions/recall-api/get-recall-bot.util.ts +0 -42
  193. package/src/logic-functions/recall-api/get-recall-recording.util.ts +0 -31
  194. package/src/logic-functions/recall-api/get-recall-webhook-bot-metadata.util.ts +0 -18
  195. package/src/logic-functions/recall-api/list-recall-transcripts.util.ts +0 -141
  196. package/src/logic-functions/recall-api/list-scheduled-recall-bots.util.ts +0 -106
  197. package/src/logic-functions/recall-api/normalize-recall-timestamp.util.ts +0 -14
  198. package/src/logic-functions/recall-api/parse-recall-webhook-event.util.ts +0 -88
  199. package/src/logic-functions/recall-api/recall-bot-api-request.util.ts +0 -165
  200. package/src/logic-functions/recall-api/recall-transcript-summary.type.ts +0 -5
  201. package/src/logic-functions/recall-api/reschedule-recall-bot.util.ts +0 -56
  202. package/src/logic-functions/recall-api/retrieve-recall-transcript.util.ts +0 -71
  203. package/src/logic-functions/recall-api/schedule-recall-bot.util.ts +0 -68
  204. package/src/logic-functions/recall-api/verify-recall-webhook-signature.util.ts +0 -109
  205. package/src/logic-functions/recall-webhook.ts +0 -90
  206. package/src/logic-functions/reconcile-call-recorder-calendar-event.ts +0 -178
  207. package/src/logic-functions/reconcile-stale-bot-state.ts +0 -106
  208. package/src/logic-functions/types/calendar-event-record.type.ts +0 -5
  209. package/src/logic-functions/types/call-recorder-policy-calendar-event-input.type.ts +0 -10
  210. package/src/logic-functions/types/call-recorder-policy-input.type.ts +0 -9
  211. package/src/logic-functions/types/call-recorder-policy-not-required-reason.type.ts +0 -5
  212. package/src/logic-functions/types/call-recorder-policy-required-reason.type.ts +0 -1
  213. package/src/logic-functions/types/call-recorder-policy-result-for-calendar-event.type.ts +0 -9
  214. package/src/logic-functions/types/call-recorder-policy-result-for-meeting.type.ts +0 -6
  215. package/src/logic-functions/types/call-recorder-policy-result.type.ts +0 -12
  216. package/src/logic-functions/types/call-recorder-reconciliation-result.type.ts +0 -16
  217. package/src/logic-functions/types/call-recording-media-file.type.ts +0 -1
  218. package/src/logic-functions/types/call-recording-record.type.ts +0 -15
  219. package/src/logic-functions/types/call-recording-update-fields.type.ts +0 -20
  220. package/src/logic-functions/types/files-field-value.type.ts +0 -1
  221. package/src/logic-functions/types/meeting-recording.type.ts +0 -7
  222. package/src/logic-functions/types/recall-bot-operation-result.type.ts +0 -19
  223. package/src/logic-functions/types/recall-routing-metadata.type.ts +0 -4
  224. package/src/logic-functions/types/removed-call-recorder-occurrence.type.ts +0 -6
  225. package/src/logic-functions/types/transcript-marker.type.ts +0 -6
  226. package/src/logic-functions/utils/as-record.util.ts +0 -6
  227. package/src/logic-functions/utils/get-application-variable-value.util.ts +0 -3
  228. package/src/logic-functions/utils/get-record-at-path.util.ts +0 -10
  229. package/src/logic-functions/utils/get-string.util.ts +0 -4
  230. package/src/logic-functions/utils/get-unique-sorted-ids.util.ts +0 -8
  231. package/src/logic-functions/utils/is-non-empty-string.util.ts +0 -5
  232. package/src/page-layouts/calendar-event-recording-tab.ts +0 -33
  233. package/src/view-fields/call-recorder-preference-on-calendar-event.view-field.ts +0 -27
  234. package/tsconfig.json +0 -42
  235. package/tsconfig.spec.json +0 -9
  236. package/vitest.config.ts +0 -31
  237. package/vitest.unit.config.ts +0 -14
@@ -1,68 +0,0 @@
1
- import { isUndefined } from '@sniptt/guards';
2
-
3
- import { getRecallBotAutomaticLeave } from 'src/logic-functions/constants/recall-bot-automatic-leave';
4
- import { getRecallBotRecordingConfig } from 'src/logic-functions/constants/recall-bot-recording-config';
5
- import { type RecallRoutingMetadata } from 'src/logic-functions/types/recall-routing-metadata.type';
6
- import { type RecallBotScheduleResult } from 'src/logic-functions/types/recall-bot-operation-result.type';
7
- import {
8
- extractRecallBotId,
9
- type RecallBotResponse,
10
- } from 'src/logic-functions/recall-api/extract-recall-bot-id.util';
11
- import { getRecallApiConfig } from 'src/logic-functions/recall-api/get-recall-api-config.util';
12
- import { recallBotApiRequest } from 'src/logic-functions/recall-api/recall-bot-api-request.util';
13
-
14
- export type ScheduleRecallBotArgs = {
15
- meetingUrl: string;
16
- joinAt: string;
17
- metadata: RecallRoutingMetadata;
18
- };
19
-
20
- export const scheduleRecallBot = async ({
21
- meetingUrl,
22
- joinAt,
23
- metadata,
24
- }: ScheduleRecallBotArgs): Promise<RecallBotScheduleResult> => {
25
- const configResult = getRecallApiConfig();
26
-
27
- if (!configResult.success) {
28
- return { ok: false, status: null, errorMessage: configResult.error };
29
- }
30
-
31
- const automaticLeave = getRecallBotAutomaticLeave();
32
-
33
- const result = await recallBotApiRequest<RecallBotResponse>({
34
- config: configResult.config,
35
- path: '/bot/',
36
- method: 'POST',
37
- body: {
38
- meeting_url: meetingUrl,
39
- join_at: joinAt,
40
- bot_name: configResult.config.botName,
41
- ...(isUndefined(automaticLeave)
42
- ? {}
43
- : { automatic_leave: automaticLeave }),
44
- recording_config: getRecallBotRecordingConfig(),
45
- metadata,
46
- },
47
- });
48
-
49
- if (!result.ok) {
50
- return result;
51
- }
52
-
53
- const externalBotId = extractRecallBotId(result.data);
54
-
55
- if (isUndefined(externalBotId)) {
56
- return {
57
- ok: false,
58
- status: null,
59
- errorMessage:
60
- 'Recall API created a bot but the response did not include a bot id',
61
- };
62
- }
63
-
64
- return {
65
- ok: true,
66
- externalBotId,
67
- };
68
- };
@@ -1,109 +0,0 @@
1
- import { createHmac, timingSafeEqual } from 'crypto';
2
-
3
- import { isUndefined } from '@sniptt/guards';
4
-
5
- const RECALL_WEBHOOK_SECRET_PREFIX = 'whsec_';
6
- const RECALL_WEBHOOK_TIMESTAMP_TOLERANCE_SECONDS = 5 * 60;
7
-
8
- export const verifyRecallWebhookSignature = ({
9
- rawBody,
10
- headers,
11
- secret,
12
- now = new Date(),
13
- }: {
14
- rawBody: string;
15
- headers: Record<string, string | undefined>;
16
- secret: string;
17
- now?: Date;
18
- }): { valid: true } | { valid: false; error: string } => {
19
- if (!secret.startsWith(RECALL_WEBHOOK_SECRET_PREFIX)) {
20
- return {
21
- valid: false,
22
- error: 'Webhook secret must start with whsec_',
23
- };
24
- }
25
-
26
- const webhookId = headers['webhook-id'] ?? headers['svix-id'];
27
- const webhookTimestamp =
28
- headers['webhook-timestamp'] ?? headers['svix-timestamp'];
29
- const webhookSignature =
30
- headers['webhook-signature'] ?? headers['svix-signature'];
31
-
32
- if (
33
- isUndefined(webhookId) ||
34
- isUndefined(webhookTimestamp) ||
35
- isUndefined(webhookSignature)
36
- ) {
37
- return {
38
- valid: false,
39
- error: 'Missing webhook signature headers',
40
- };
41
- }
42
-
43
- const webhookTimestampSeconds = Number(webhookTimestamp);
44
-
45
- if (!Number.isInteger(webhookTimestampSeconds)) {
46
- return {
47
- valid: false,
48
- error: 'Invalid webhook timestamp',
49
- };
50
- }
51
-
52
- const nowSeconds = Math.floor(now.getTime() / 1000);
53
-
54
- if (
55
- Math.abs(nowSeconds - webhookTimestampSeconds) >
56
- RECALL_WEBHOOK_TIMESTAMP_TOLERANCE_SECONDS
57
- ) {
58
- return {
59
- valid: false,
60
- error: 'Webhook timestamp is outside of the allowed tolerance',
61
- };
62
- }
63
-
64
- const secretBytes = Buffer.from(
65
- secret.slice(RECALL_WEBHOOK_SECRET_PREFIX.length),
66
- 'base64',
67
- );
68
- const expectedSignature = createHmac('sha256', secretBytes)
69
- .update(`${webhookId}.${webhookTimestamp}.${rawBody}`)
70
- .digest('base64');
71
- const providedSignatures = webhookSignature
72
- .split(' ')
73
- .map((signaturePart) => signaturePart.trim())
74
- .filter((signaturePart) => signaturePart !== '')
75
- .flatMap((signaturePart) => {
76
- if (signaturePart.startsWith('v1,') || signaturePart.startsWith('v1=')) {
77
- return [signaturePart.slice(3).trim()];
78
- }
79
-
80
- return [];
81
- })
82
- .filter((signaturePart) => signaturePart !== '');
83
-
84
- if (providedSignatures.length === 0) {
85
- return {
86
- valid: false,
87
- error: 'Missing v1 signature',
88
- };
89
- }
90
-
91
- const expectedSignatureBuffer = Buffer.from(expectedSignature, 'base64');
92
-
93
- for (const providedSignature of providedSignatures) {
94
- const providedSignatureBuffer = Buffer.from(providedSignature, 'base64');
95
-
96
- if (providedSignatureBuffer.length !== expectedSignatureBuffer.length) {
97
- continue;
98
- }
99
-
100
- if (timingSafeEqual(providedSignatureBuffer, expectedSignatureBuffer)) {
101
- return { valid: true };
102
- }
103
- }
104
-
105
- return {
106
- valid: false,
107
- error: 'Signature verification failed',
108
- };
109
- };
@@ -1,90 +0,0 @@
1
- import { isNull, isUndefined } from '@sniptt/guards';
2
- import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define';
3
-
4
- import { PROCESS_RECALL_WEBHOOK_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER } from 'src/constants/process-recall-webhook-logic-function-universal-identifier';
5
- import { RECALL_WEBHOOK_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER } from 'src/constants/recall-webhook-logic-function-universal-identifier';
6
- import { RECALL_WEBHOOK_SECRET_ENV_VAR_NAME } from 'src/logic-functions/constants/recall-webhook-secret-env-var-name';
7
- import { extractTwentyWorkspaceIdFromRecallWebhook } from 'src/logic-functions/recall-api/extract-twenty-workspace-id-from-recall-webhook.util';
8
- import { type RecallWebhookBody } from 'src/logic-functions/recall-api/parse-recall-webhook-event.util';
9
- import { verifyRecallWebhookSignature } from 'src/logic-functions/recall-api/verify-recall-webhook-signature.util';
10
- import { getApplicationVariableValue } from 'src/logic-functions/utils/get-application-variable-value.util';
11
- import { isNonEmptyString } from 'src/logic-functions/utils/is-non-empty-string.util';
12
-
13
- type RecallWebhookResolverResult = {
14
- workspaceId: string;
15
- targetLogicFunctionUniversalIdentifier: string;
16
- payload: RecallWebhookBody;
17
- };
18
-
19
- // A thrown error becomes a non-2xx, which makes Svix retry; a returned result dispatches to the target.
20
- export const recallWebhookRouteHandler = (
21
- routePayload: RoutePayload<RecallWebhookBody>,
22
- ): RecallWebhookResolverResult => {
23
- const webhookSecret = getApplicationVariableValue(
24
- RECALL_WEBHOOK_SECRET_ENV_VAR_NAME,
25
- );
26
-
27
- if (!isNonEmptyString(webhookSecret)) {
28
- throw new Error(
29
- 'RECALL_WEBHOOK_SECRET server variable is not set. A server admin must copy it from the Recall webhook endpoint settings and set it on the Call Recorder application registration.',
30
- );
31
- }
32
-
33
- const { rawBody } = routePayload;
34
-
35
- if (isUndefined(rawBody)) {
36
- throw new Error(
37
- 'Raw request body was not forwarded by the server; cannot verify the webhook signature',
38
- );
39
- }
40
-
41
- const signatureCheck = verifyRecallWebhookSignature({
42
- rawBody,
43
- headers: routePayload.headers,
44
- secret: webhookSecret,
45
- });
46
-
47
- if (!signatureCheck.valid) {
48
- throw new Error(`Invalid webhook signature: ${signatureCheck.error}`);
49
- }
50
-
51
- const body = routePayload.body;
52
-
53
- if (isUndefined(body) || isNull(body)) {
54
- throw new Error('Webhook payload was empty');
55
- }
56
-
57
- const workspaceId = extractTwentyWorkspaceIdFromRecallWebhook(body);
58
-
59
- if (!isNonEmptyString(workspaceId)) {
60
- throw new Error(
61
- 'Webhook payload is missing the Twenty workspace id in the Recall bot metadata',
62
- );
63
- }
64
-
65
- return {
66
- workspaceId,
67
- targetLogicFunctionUniversalIdentifier:
68
- PROCESS_RECALL_WEBHOOK_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER,
69
- payload: body,
70
- };
71
- };
72
-
73
- export default defineLogicFunction({
74
- universalIdentifier: RECALL_WEBHOOK_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER,
75
- name: 'recall-webhook',
76
- description:
77
- 'Verifies Recall.ai webhook signatures and resolves the target workspace for the matching CallRecording update.',
78
- timeoutSeconds: 30,
79
- handler: recallWebhookRouteHandler,
80
- serverRouteTriggerSettings: {
81
- forwardedRequestHeaders: [
82
- 'webhook-id',
83
- 'webhook-timestamp',
84
- 'webhook-signature',
85
- 'svix-id',
86
- 'svix-timestamp',
87
- 'svix-signature',
88
- ],
89
- },
90
- });
@@ -1,178 +0,0 @@
1
- import { isUndefined } from '@sniptt/guards';
2
- import { CoreApiClient } from 'twenty-client-sdk/core';
3
- import {
4
- defineLogicFunction,
5
- type DatabaseEventPayload,
6
- type ObjectRecordBaseEvent,
7
- } from 'twenty-sdk/define';
8
-
9
- import { CALENDAR_EVENT_RECONCILIATION_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER } from 'src/constants/calendar-event-reconciliation-logic-function-universal-identifier';
10
- import { type RemovedCallRecorderOccurrence } from 'src/logic-functions/types/removed-call-recorder-occurrence.type';
11
- import { computeRealMeetingKey } from 'src/logic-functions/domain/compute-real-meeting-key.util';
12
- import { getUniqueSortedIds } from 'src/logic-functions/utils/get-unique-sorted-ids.util';
13
- import { reconcileCallRecorderForCalendarEventIds } from 'src/logic-functions/flows/reconcile-call-recorder.util';
14
-
15
- const CALENDAR_EVENT_OBJECT_NAME = 'calendarEvent';
16
-
17
- const CALL_RECORDER_RELEVANT_CALENDAR_EVENT_FIELDS = [
18
- 'title',
19
- 'callRecorderPreference',
20
- 'conferenceLink',
21
- 'startsAt',
22
- 'endsAt',
23
- 'isCanceled',
24
- 'iCalUid',
25
- ];
26
-
27
- const CALL_RECORDER_KEY_CALENDAR_EVENT_FIELDS = [
28
- 'conferenceLink',
29
- 'startsAt',
30
- 'iCalUid',
31
- ];
32
-
33
- type CalendarEventForDatabaseEvent = {
34
- id: string;
35
- conferenceLink?: { primaryLinkUrl?: string | null } | null;
36
- iCalUid?: string | null;
37
- startsAt?: string | null;
38
- };
39
-
40
- type CalendarEventDatabaseEvent = DatabaseEventPayload<
41
- ObjectRecordBaseEvent<CalendarEventForDatabaseEvent>
42
- >;
43
-
44
- type CalendarEventReconciliationPayload = {
45
- calendarEventIds: string[];
46
- removedOccurrences: RemovedCallRecorderOccurrence[];
47
- };
48
-
49
- const handler = async (
50
- event: CalendarEventDatabaseEvent,
51
- ): Promise<object | undefined> => {
52
- const [objectName, action] = event.name.split('.');
53
-
54
- if (objectName !== CALENDAR_EVENT_OBJECT_NAME) {
55
- return { skipped: true, reason: 'not a calendar event' };
56
- }
57
-
58
- const reconciliationPayload = buildCalendarEventReconciliationPayload({
59
- event,
60
- action,
61
- });
62
-
63
- if (
64
- reconciliationPayload.calendarEventIds.length === 0 &&
65
- reconciliationPayload.removedOccurrences.length === 0
66
- ) {
67
- return { skipped: true, reason: 'no relevant calendar event change' };
68
- }
69
-
70
- const client = new CoreApiClient();
71
- const reconciliationResults = await reconcileCallRecorderForCalendarEventIds({
72
- client,
73
- calendarEventIds: reconciliationPayload.calendarEventIds,
74
- removedOccurrences: reconciliationPayload.removedOccurrences,
75
- });
76
-
77
- return {
78
- reconciled: true,
79
- calendarEventIds: reconciliationPayload.calendarEventIds,
80
- removedOccurrenceCount: reconciliationPayload.removedOccurrences.length,
81
- reconciliationResults,
82
- };
83
- };
84
-
85
- const buildCalendarEventReconciliationPayload = ({
86
- event,
87
- action,
88
- }: {
89
- event: CalendarEventDatabaseEvent;
90
- action: string | undefined;
91
- }): CalendarEventReconciliationPayload => {
92
- if (action === 'created') {
93
- return {
94
- calendarEventIds: getUniqueSortedIds([
95
- event.recordId,
96
- event.properties.after?.id,
97
- ]),
98
- removedOccurrences: [],
99
- };
100
- }
101
-
102
- if (action === 'updated') {
103
- const updatedFields = event.properties.updatedFields ?? [];
104
-
105
- if (!hasRelevantFieldChange(updatedFields)) {
106
- return { calendarEventIds: [], removedOccurrences: [] };
107
- }
108
-
109
- const removedOccurrence = hasKeyFieldChange(updatedFields)
110
- ? buildRemovedOccurrence(event.properties.before)
111
- : undefined;
112
-
113
- return {
114
- calendarEventIds: getUniqueSortedIds([
115
- event.recordId,
116
- event.properties.after?.id,
117
- ]),
118
- removedOccurrences: isUndefined(removedOccurrence)
119
- ? []
120
- : [removedOccurrence],
121
- };
122
- }
123
-
124
- if (action === 'deleted' || action === 'destroyed') {
125
- const removedOccurrence = buildRemovedOccurrence(event.properties.before);
126
-
127
- return {
128
- calendarEventIds: [],
129
- removedOccurrences: isUndefined(removedOccurrence)
130
- ? []
131
- : [removedOccurrence],
132
- };
133
- }
134
-
135
- return { calendarEventIds: [], removedOccurrences: [] };
136
- };
137
-
138
- const hasRelevantFieldChange = (updatedFields: string[]): boolean =>
139
- updatedFields.some((updatedField) =>
140
- CALL_RECORDER_RELEVANT_CALENDAR_EVENT_FIELDS.includes(updatedField),
141
- );
142
-
143
- const hasKeyFieldChange = (updatedFields: string[]): boolean =>
144
- updatedFields.some((updatedField) =>
145
- CALL_RECORDER_KEY_CALENDAR_EVENT_FIELDS.includes(updatedField),
146
- );
147
-
148
- const buildRemovedOccurrence = (
149
- calendarEvent: CalendarEventForDatabaseEvent | undefined,
150
- ): RemovedCallRecorderOccurrence | undefined => {
151
- if (isUndefined(calendarEvent)) {
152
- return undefined;
153
- }
154
-
155
- return {
156
- calendarEventId: calendarEvent.id,
157
- realMeetingKey: computeRealMeetingKey({
158
- calendarEventId: calendarEvent.id,
159
- conferenceLinkUrl: calendarEvent.conferenceLink?.primaryLinkUrl,
160
- iCalUid: calendarEvent.iCalUid ?? undefined,
161
- startsAt: calendarEvent.startsAt ?? undefined,
162
- }),
163
- startsAt: calendarEvent.startsAt ?? undefined,
164
- };
165
- };
166
-
167
- export default defineLogicFunction({
168
- universalIdentifier:
169
- CALENDAR_EVENT_RECONCILIATION_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER,
170
- name: 'reconcile-call-recorder-calendar-event',
171
- description:
172
- 'Reconciles app-managed Recall bot recording requests when calendar events change.',
173
- timeoutSeconds: 60,
174
- handler,
175
- databaseEventTriggerSettings: {
176
- eventName: `${CALENDAR_EVENT_OBJECT_NAME}.*`,
177
- },
178
- });
@@ -1,106 +0,0 @@
1
- import { CoreApiClient } from 'twenty-client-sdk/core';
2
- import { defineLogicFunction } from 'twenty-sdk/define';
3
-
4
- import { STALE_BOT_STATE_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER } from 'src/constants/stale-bot-state-logic-function-universal-identifier';
5
- import { STALE_BOT_STATE_CRON_PATTERN } from 'src/logic-functions/constants/stale-bot-state-cron-pattern';
6
- import { convergeDivergedCallRecordings } from 'src/logic-functions/flows/converge-diverged-call-recordings.util';
7
- import { type ConvergeDivergedCallRecordingsResult } from 'src/logic-functions/flows/converge-diverged-call-recordings-result.type';
8
- import {
9
- healCallRecordingsMissingBot,
10
- type HealCallRecordingsMissingBotResult,
11
- } from 'src/logic-functions/flows/heal-call-recordings-missing-bot.util';
12
- import {
13
- reapOrphanedCallRecorders,
14
- type ReapOrphanedCallRecordersResult,
15
- } from 'src/logic-functions/flows/reap-orphaned-call-recorders.util';
16
-
17
- // Every unwanted bot passes through this join_at window before it can attend.
18
- const REAPER_JOIN_AT_LOOKBACK_HOURS = 4;
19
- const REAPER_JOIN_AT_LOOKAHEAD_HOURS = 24;
20
-
21
- type StepFailure = { error: string };
22
-
23
- const reconcileStaleBotStateHandler = async (): Promise<object> => {
24
- const now = new Date();
25
- const client = new CoreApiClient();
26
-
27
- const botlessHealResult = await healCallRecordingsMissingBotSafely(
28
- client,
29
- now,
30
- );
31
- const orphanedBotReapingResult =
32
- await reapOrphanedCallRecordersInJoinAtWindow(client, now);
33
- const statusConvergenceResult = await convergeDivergedCallRecordingsSafely(
34
- client,
35
- now,
36
- );
37
-
38
- return {
39
- botlessHealResult,
40
- orphanedBotReapingResult,
41
- statusConvergenceResult,
42
- };
43
- };
44
-
45
- const healCallRecordingsMissingBotSafely = async (
46
- client: CoreApiClient,
47
- now: Date,
48
- ): Promise<HealCallRecordingsMissingBotResult | StepFailure> => {
49
- try {
50
- return await healCallRecordingsMissingBot({ client, now });
51
- } catch (error) {
52
- return buildStepFailure('botless call recording healing', error);
53
- }
54
- };
55
-
56
- const reapOrphanedCallRecordersInJoinAtWindow = async (
57
- client: CoreApiClient,
58
- now: Date,
59
- ): Promise<ReapOrphanedCallRecordersResult | StepFailure> => {
60
- try {
61
- return await reapOrphanedCallRecorders({
62
- client,
63
- joinAtAfter: new Date(
64
- now.getTime() - REAPER_JOIN_AT_LOOKBACK_HOURS * 60 * 60 * 1000,
65
- ).toISOString(),
66
- joinAtBefore: new Date(
67
- now.getTime() + REAPER_JOIN_AT_LOOKAHEAD_HOURS * 60 * 60 * 1000,
68
- ).toISOString(),
69
- });
70
- } catch (error) {
71
- return buildStepFailure('orphaned bot reaping', error);
72
- }
73
- };
74
-
75
- const convergeDivergedCallRecordingsSafely = async (
76
- client: CoreApiClient,
77
- now: Date,
78
- ): Promise<ConvergeDivergedCallRecordingsResult | StepFailure> => {
79
- try {
80
- return await convergeDivergedCallRecordings({ client, now });
81
- } catch (error) {
82
- return buildStepFailure('call recording status convergence', error);
83
- }
84
- };
85
-
86
- const buildStepFailure = (stepLabel: string, error: unknown): StepFailure => {
87
- const errorMessage = error instanceof Error ? error.message : String(error);
88
-
89
- if (process.env.NODE_ENV !== 'test') {
90
- console.error(`[call-recorder] ${stepLabel} failed: ${errorMessage}`);
91
- }
92
-
93
- return { error: `${stepLabel} failed` };
94
- };
95
-
96
- export default defineLogicFunction({
97
- universalIdentifier: STALE_BOT_STATE_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER,
98
- name: 'reconcile-stale-bot-state',
99
- description:
100
- 'Converges call recordings with Recall on a schedule: pulls stale bot statuses and overdue transcripts, finishes failed cancellations, schedules bots for recordings still missing one, and reaps unclaimed bots. Reads calendar events only to heal already-decided recordings, never to discover meetings.',
101
- timeoutSeconds: 250,
102
- handler: reconcileStaleBotStateHandler,
103
- cronTriggerSettings: {
104
- pattern: STALE_BOT_STATE_CRON_PATTERN,
105
- },
106
- });
@@ -1,5 +0,0 @@
1
- import { type CallRecorderPolicyCalendarEventInput } from 'src/logic-functions/types/call-recorder-policy-calendar-event-input.type';
2
-
3
- export type CalendarEventRecord = CallRecorderPolicyCalendarEventInput & {
4
- title: string | undefined;
5
- };
@@ -1,10 +0,0 @@
1
- // Domain read shape: wire composites are flattened and absence is undefined.
2
- export type CallRecorderPolicyCalendarEventInput = {
3
- id: string;
4
- isCanceled: boolean;
5
- startsAt: string | undefined;
6
- endsAt: string | undefined;
7
- iCalUid: string | undefined;
8
- conferenceLinkUrl: string | undefined;
9
- callRecorderPreference: string | undefined;
10
- };
@@ -1,9 +0,0 @@
1
- import { type CallRecorderPreference } from 'src/constants/call-recorder-preference';
2
-
3
- export type CallRecorderPolicyInput = {
4
- callRecorderPreference: CallRecorderPreference | undefined;
5
- isCanceled: boolean;
6
- startsAt: string | undefined;
7
- endsAt: string | undefined;
8
- conferenceLinkUrl: string | undefined;
9
- };
@@ -1,5 +0,0 @@
1
- export type CallRecorderPolicyNotRequiredReason =
2
- | 'EVENT_CANCELED'
3
- | 'PREFERENCE_OFF'
4
- | 'MISSING_CONFERENCE_LINK'
5
- | 'EVENT_NOT_UPCOMING';
@@ -1 +0,0 @@
1
- export type CallRecorderPolicyRequiredReason = 'RECORDING_ENABLED';
@@ -1,9 +0,0 @@
1
- import { type CallRecorderPreference } from 'src/constants/call-recorder-preference';
2
- import { type CallRecorderPolicyResult } from 'src/logic-functions/types/call-recorder-policy-result.type';
3
-
4
- export type CallRecorderPolicyResultForCalendarEvent =
5
- CallRecorderPolicyResult & {
6
- calendarEventId: string;
7
- callRecorderPreference: CallRecorderPreference | undefined;
8
- realMeetingKey: string;
9
- };
@@ -1,6 +0,0 @@
1
- export type CallRecorderPolicyResultForMeeting = {
2
- realMeetingKey: string;
3
- shouldRequestBot: boolean;
4
- calendarEventIds: string[];
5
- requestingCalendarEventIds: string[];
6
- };
@@ -1,12 +0,0 @@
1
- import { type CallRecorderPolicyNotRequiredReason } from 'src/logic-functions/types/call-recorder-policy-not-required-reason.type';
2
- import { type CallRecorderPolicyRequiredReason } from 'src/logic-functions/types/call-recorder-policy-required-reason.type';
3
-
4
- export type CallRecorderPolicyResult =
5
- | {
6
- shouldRequestBot: true;
7
- reason: CallRecorderPolicyRequiredReason;
8
- }
9
- | {
10
- shouldRequestBot: false;
11
- reason: CallRecorderPolicyNotRequiredReason;
12
- };
@@ -1,16 +0,0 @@
1
- export type CallRecorderReconciliationResult =
2
- | {
3
- action: 'CREATED' | 'UPDATED' | 'CANCELED';
4
- realMeetingKey: string;
5
- callRecordingId: string;
6
- }
7
- | {
8
- action: 'SKIPPED';
9
- realMeetingKey: string;
10
- callRecordingId: string | null;
11
- }
12
- | {
13
- action: 'FAILED';
14
- realMeetingKey: string;
15
- errorMessage: string;
16
- };
@@ -1 +0,0 @@
1
- export type CallRecordingMediaFile = { fileId: string; label: string };
@@ -1,15 +0,0 @@
1
- import { type CallRecordingRequestStatus } from 'src/logic-functions/constants/call-recording-request-status';
2
-
3
- // Domain read shape: absence is always undefined; null lives only on wire types.
4
- export type CallRecordingRecord = {
5
- id: string;
6
- title?: string;
7
- status?: string;
8
- recordingRequestStatus?: CallRecordingRequestStatus;
9
- startedAt?: string;
10
- endedAt?: string;
11
- calendarEventId?: string;
12
- externalBotId?: string;
13
- externalRecordingId?: string;
14
- callRecorderFailureReason?: string;
15
- };
@@ -1,20 +0,0 @@
1
- import { type CallRecordingRequestStatus } from 'src/logic-functions/constants/call-recording-request-status';
2
- import { type CallRecordingStatus } from 'src/logic-functions/constants/call-recording-status';
3
- import { type CallRecordingMediaFile } from 'src/logic-functions/types/call-recording-media-file.type';
4
-
5
- export type CallRecordingUpdateFields = Partial<{
6
- // null clears a previously synced title when the calendar title disappears.
7
- title: string | null;
8
- status: CallRecordingStatus;
9
- recordingRequestStatus: CallRecordingRequestStatus;
10
- startedAt: string;
11
- endedAt: string;
12
- calendarEventId: string;
13
- // null clears stale app-owned state on cancel/eject or reschedule.
14
- externalBotId: string | null;
15
- externalRecordingId: string;
16
- callRecorderFailureReason: string | null;
17
- transcript: Record<string, unknown>;
18
- audio: CallRecordingMediaFile[];
19
- video: CallRecordingMediaFile[];
20
- }>;
@@ -1 +0,0 @@
1
- export type FilesFieldValue = { fileId: string }[];