chore(call-recorder): drop FAILED status schema bridge (#22134)
Cleanup of the call-recorder data layer + the SDK 2.16 bump and its fallout, now that the `FAILED_UNKNOWN → FAILED` rename shipped in `twenty/v2.16.0` (#22062). - **Drop the schema bridge.** `executeCurrentSchemaMutation` and the integration-test compatibility filter existed only to work around servers exposing `FAILED_UNKNOWN`. Deleted the bridge; `updateCallRecording` / `completeCallRecordingIngestion` call `client.mutation(...)` directly. Integration test iterates all `CallRecordingStatus` values. - **Bump SDK** `twenty-sdk` / `twenty-client-sdk` to `2.16.0`. - **One export per file.** Move `CallRecordingUpdateFields` to its own type file; extract the duplicated media-file shape into `CallRecordingMediaFile`. - **Migrate the Recall webhook to `serverRouteTriggerSettings`** (2.16 dropped `serverWebhookTriggerSettings` + its declarative `workspaceIdResolver`). The webhook is now a **resolver** (`recall-webhook`) that verifies the Svix signature, reads `twentyWorkspaceId` from the Recall bot metadata, and returns `{ workspaceId, targetLogicFunctionUniversalIdentifier, payload }`; the platform dispatches to a new **target** function (`process-recall-webhook`) in the resolved workspace, where `CoreApiClient` is workspace-scoped. Resolver UID/route unchanged, so the registered Recall endpoint URL stays valid. Failures now throw → HTTP 500 (Svix retries) instead of returning 401/400. Verified: typecheck, 213 unit tests, oxlint, oxfmt all green. **Not yet verified end-to-end against a live server** — call-recorder is the first app on `serverRouteTriggerSettings`, so a real Recall webhook should be tested through the resolver→target path before relying on it.
This commit is contained in:
@@ -106,8 +106,8 @@ What this app intentionally does **not** do in v1:
|
||||
| No bot joined a meeting | **Recording Bot** was Off, the event had no conference link, it wasn't synced from a connected calendar, or `RECALL_API_KEY` isn't set | Confirm the event is On, upcoming, has a video link, and came from a synced calendar; admin: confirm `RECALL_API_KEY` is set |
|
||||
| Recording never reaches `COMPLETED` | A Recall webhook was missed, or only one of audio/video was produced | The reconciliation job pulls the latest status from Recall within a few minutes; if it is marked `FAILED`, inspect the bot in the Recall dashboard |
|
||||
| Transcript empty, or marked pending/failed | Recall hasn't finished async transcription yet, or transcription failed for that call | Wait for the reconciliation job to ingest the transcript; a persistent failure leaves a marker in the transcript |
|
||||
| Webhook rejected with `401` (Recall keeps retrying) | `RECALL_WEBHOOK_SECRET` doesn't match the Recall endpoint's signing secret | Re-copy the `whsec_…` secret from the Recall webhook endpoint into the `RECALL_WEBHOOK_SECRET` server variable |
|
||||
| Webhook rejected with `500` about the secret | `RECALL_WEBHOOK_SECRET` is not set | Admin: set it on the application registration |
|
||||
| Webhook rejected with `500` (`Invalid webhook signature`, Recall keeps retrying) | `RECALL_WEBHOOK_SECRET` doesn't match the Recall endpoint's signing secret | Re-copy the `whsec_…` secret from the Recall webhook endpoint into the `RECALL_WEBHOOK_SECRET` server variable |
|
||||
| Webhook rejected with `500` (`RECALL_WEBHOOK_SECRET … not set`) | `RECALL_WEBHOOK_SECRET` is not set | Admin: set it on the application registration |
|
||||
| Bot left almost immediately | No one was admitted before the lobby / no-one-joined timeout, or everyone left | Adjust `CALL_RECORDER_WAITING_ROOM_TIMEOUT_SECONDS` / `CALL_RECORDER_NOONE_JOINED_TIMEOUT_SECONDS` if too aggressive |
|
||||
| Bot joined a meeting you didn't want recorded | Recording is on by default | Set the event's **Recording Bot** field to Off; the scheduled bot is canceled |
|
||||
|
||||
|
||||
@@ -31,8 +31,8 @@
|
||||
"oxlint": "^0.16.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"twenty-client-sdk": "2.15.0",
|
||||
"twenty-sdk": "2.15.0",
|
||||
"twenty-client-sdk": "2.16.0",
|
||||
"twenty-sdk": "2.16.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vite-tsconfig-paths": "^4.2.1",
|
||||
"vitest": "^4.1.9"
|
||||
|
||||
@@ -5,10 +5,6 @@ import { describe, expect, it } from 'vitest';
|
||||
import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/constants/application-universal-identifier';
|
||||
import { CallRecordingRequestStatus } from 'src/logic-functions/constants/call-recording-request-status';
|
||||
import { CallRecordingStatus } from 'src/logic-functions/constants/call-recording-status';
|
||||
import {
|
||||
executeCurrentSchemaMutation,
|
||||
type CurrentSchemaUpdateCallRecordingMutation,
|
||||
} from 'src/logic-functions/data/execute-current-schema-mutation.util';
|
||||
|
||||
describe('App installation', () => {
|
||||
it('should find the installed app in the applications list', async () => {
|
||||
@@ -58,30 +54,16 @@ describe('CallRecording status contract', () => {
|
||||
}
|
||||
|
||||
expect(serverCallRecordingStatuses).toEqual(
|
||||
expect.arrayContaining([
|
||||
CallRecordingStatus.SCHEDULED,
|
||||
CallRecordingStatus.JOINING,
|
||||
CallRecordingStatus.RECORDING,
|
||||
CallRecordingStatus.PROCESSING,
|
||||
CallRecordingStatus.COMPLETED,
|
||||
]),
|
||||
expect.arrayContaining(Object.values(CallRecordingStatus)),
|
||||
);
|
||||
|
||||
// TODO: Remove this compatibility filter once the released server/SDK
|
||||
// exposes FAILED instead of FAILED_UNKNOWN.
|
||||
const statusesAcceptedByCurrentServer = Object.values(
|
||||
CallRecordingStatus,
|
||||
).filter((status) => serverCallRecordingStatuses.includes(status));
|
||||
|
||||
for (const status of statusesAcceptedByCurrentServer) {
|
||||
const mutation = {
|
||||
for (const status of Object.values(CallRecordingStatus)) {
|
||||
const updated = await client.mutation({
|
||||
updateCallRecording: {
|
||||
__args: { id: callRecordingId, data: { status } },
|
||||
status: true,
|
||||
},
|
||||
} satisfies CurrentSchemaUpdateCallRecordingMutation;
|
||||
|
||||
const updated = await executeCurrentSchemaMutation(client, mutation);
|
||||
});
|
||||
|
||||
expect(updated.updateCallRecording?.status).toBe(status);
|
||||
}
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export const PROCESS_RECALL_WEBHOOK_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER =
|
||||
'13d9c427-447e-494a-8d3c-1af5d0bacb82';
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import processRecallWebhookLogicFunction, {
|
||||
processRecallWebhookHandler,
|
||||
} from 'src/logic-functions/process-recall-webhook';
|
||||
|
||||
const handleRecallWebhookMock = vi.hoisted(() => vi.fn());
|
||||
const coreApiClientMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock('src/logic-functions/flows/handle-recall-webhook.util', () => ({
|
||||
handleRecallWebhook: handleRecallWebhookMock,
|
||||
}));
|
||||
|
||||
vi.mock('twenty-client-sdk/core', () => ({
|
||||
CoreApiClient: coreApiClientMock,
|
||||
}));
|
||||
|
||||
const buildRecordingDoneWebhookBody = () => ({
|
||||
event: 'recording.done',
|
||||
data: {
|
||||
bot: {
|
||||
id: 'recall-bot-1',
|
||||
metadata: {
|
||||
twentyWorkspaceId: '123e4567-e89b-12d3-a456-426614174000',
|
||||
twentyCallRecordingId: 'call-recording-1',
|
||||
},
|
||||
},
|
||||
recording: { id: 'recall-recording-1' },
|
||||
},
|
||||
});
|
||||
|
||||
describe('process-recall-webhook', () => {
|
||||
beforeEach(() => {
|
||||
handleRecallWebhookMock.mockReset();
|
||||
handleRecallWebhookMock.mockResolvedValue({ status: 'updated' });
|
||||
coreApiClientMock.mockReset();
|
||||
});
|
||||
|
||||
it('declares no external trigger so it only runs when dispatched by the resolver', () => {
|
||||
expect(processRecallWebhookLogicFunction.success).toBe(true);
|
||||
expect(
|
||||
'serverRouteTriggerSettings' in processRecallWebhookLogicFunction.config,
|
||||
).toBe(false);
|
||||
expect(
|
||||
processRecallWebhookLogicFunction.config.httpRouteTriggerSettings,
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('forwards the resolved payload to handleRecallWebhook with a workspace-scoped client', async () => {
|
||||
const body = buildRecordingDoneWebhookBody();
|
||||
|
||||
const result = await processRecallWebhookHandler(body);
|
||||
|
||||
expect(coreApiClientMock).toHaveBeenCalledTimes(1);
|
||||
expect(handleRecallWebhookMock).toHaveBeenCalledTimes(1);
|
||||
expect(handleRecallWebhookMock).toHaveBeenCalledWith({
|
||||
client: coreApiClientMock.mock.instances[0],
|
||||
body,
|
||||
});
|
||||
expect(result).toEqual({ status: 'updated' });
|
||||
});
|
||||
});
|
||||
+71
-85
@@ -2,12 +2,12 @@ import { createHmac } from 'crypto';
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { PROCESS_RECALL_WEBHOOK_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER } from 'src/constants/process-recall-webhook-logic-function-universal-identifier';
|
||||
import recallWebhookLogicFunction, {
|
||||
recallWebhookRouteHandler,
|
||||
} from 'src/logic-functions/recall-webhook';
|
||||
|
||||
const getApplicationVariableValueMock = vi.hoisted(() => vi.fn());
|
||||
const handleRecallWebhookMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock(
|
||||
'src/logic-functions/utils/get-application-variable-value.util',
|
||||
@@ -16,17 +16,10 @@ vi.mock(
|
||||
}),
|
||||
);
|
||||
|
||||
vi.mock('src/logic-functions/flows/handle-recall-webhook.util', () => ({
|
||||
handleRecallWebhook: handleRecallWebhookMock,
|
||||
}));
|
||||
|
||||
vi.mock('twenty-client-sdk/core', () => ({
|
||||
CoreApiClient: vi.fn(),
|
||||
}));
|
||||
|
||||
const SECRET_BYTES = Buffer.from('entry-test-secret');
|
||||
const SECRET = `whsec_${SECRET_BYTES.toString('base64')}`;
|
||||
const WORKSPACE_ID = '123e4567-e89b-12d3-a456-426614174000';
|
||||
const CALL_RECORDING_ID = 'call-recording-1';
|
||||
|
||||
type RecallWebhookRoutePayload = Parameters<
|
||||
typeof recallWebhookRouteHandler
|
||||
@@ -61,6 +54,7 @@ const buildRecordingDoneWebhookBody = () => ({
|
||||
id: 'recall-bot-1',
|
||||
metadata: {
|
||||
twentyWorkspaceId: WORKSPACE_ID,
|
||||
twentyCallRecordingId: CALL_RECORDING_ID,
|
||||
},
|
||||
},
|
||||
recording: {
|
||||
@@ -71,25 +65,26 @@ const buildRecordingDoneWebhookBody = () => ({
|
||||
|
||||
describe('recallWebhookRouteHandler', () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
getApplicationVariableValueMock.mockReset();
|
||||
getApplicationVariableValueMock.mockReturnValue(SECRET);
|
||||
handleRecallWebhookMock.mockReset();
|
||||
handleRecallWebhookMock.mockResolvedValue({ status: 'updated' });
|
||||
});
|
||||
|
||||
it('declares a server webhook resolver for Recall bot workspace metadata', () => {
|
||||
it('declares a server route trigger that forwards the webhook signature headers', () => {
|
||||
expect(recallWebhookLogicFunction.success).toBe(true);
|
||||
expect(
|
||||
recallWebhookLogicFunction.config.httpRouteTriggerSettings,
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
recallWebhookLogicFunction.config.serverWebhookTriggerSettings,
|
||||
'serverRouteTriggerSettings' in recallWebhookLogicFunction.config,
|
||||
).toBe(true);
|
||||
|
||||
if (!('serverRouteTriggerSettings' in recallWebhookLogicFunction.config)) {
|
||||
throw new Error('Expected a server route trigger');
|
||||
}
|
||||
|
||||
expect(
|
||||
recallWebhookLogicFunction.config.serverRouteTriggerSettings,
|
||||
).toEqual({
|
||||
workspaceIdResolver: {
|
||||
source: 'body',
|
||||
path: 'data.bot.metadata.twentyWorkspaceId',
|
||||
},
|
||||
forwardedRequestHeaders: [
|
||||
'webhook-id',
|
||||
'webhook-timestamp',
|
||||
@@ -101,83 +96,73 @@ describe('recallWebhookRouteHandler', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('responds 500 when the webhook secret is not configured', async () => {
|
||||
it('throws when the webhook secret is not configured', () => {
|
||||
getApplicationVariableValueMock.mockReturnValue(undefined);
|
||||
|
||||
const result = await recallWebhookRouteHandler(
|
||||
buildRoutePayload({ rawBody: '{}', body: {} }),
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
__twentyHttpResponse: true,
|
||||
status: 500,
|
||||
body: {
|
||||
error: expect.stringContaining('RECALL_WEBHOOK_SECRET'),
|
||||
},
|
||||
});
|
||||
expect(() =>
|
||||
recallWebhookRouteHandler(buildRoutePayload({ rawBody: '{}', body: {} })),
|
||||
).toThrow('RECALL_WEBHOOK_SECRET');
|
||||
});
|
||||
|
||||
it('responds 500 when the raw body is not forwarded', async () => {
|
||||
const result = await recallWebhookRouteHandler(
|
||||
buildRoutePayload({ body: {} }),
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
__twentyHttpResponse: true,
|
||||
status: 500,
|
||||
body: {
|
||||
error: expect.stringContaining('Raw request body'),
|
||||
},
|
||||
});
|
||||
it('throws when the raw body is not forwarded', () => {
|
||||
expect(() =>
|
||||
recallWebhookRouteHandler(buildRoutePayload({ body: {} })),
|
||||
).toThrow('Raw request body');
|
||||
});
|
||||
|
||||
it('responds 401 when the signature is invalid', async () => {
|
||||
const result = await recallWebhookRouteHandler(
|
||||
buildRoutePayload({
|
||||
rawBody: '{}',
|
||||
body: {},
|
||||
headers: {
|
||||
'webhook-id': 'msg_entry_test',
|
||||
'webhook-timestamp': Math.floor(Date.now() / 1000).toString(),
|
||||
'webhook-signature': 'v1,not-a-real-signature',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
__twentyHttpResponse: true,
|
||||
status: 401,
|
||||
body: {
|
||||
error: expect.stringContaining('Invalid webhook signature'),
|
||||
},
|
||||
});
|
||||
it('throws when the signature is invalid', () => {
|
||||
expect(() =>
|
||||
recallWebhookRouteHandler(
|
||||
buildRoutePayload({
|
||||
rawBody: '{}',
|
||||
body: {},
|
||||
headers: {
|
||||
'webhook-id': 'msg_entry_test',
|
||||
'webhook-timestamp': Math.floor(Date.now() / 1000).toString(),
|
||||
'webhook-signature': 'v1,not-a-real-signature',
|
||||
},
|
||||
}),
|
||||
),
|
||||
).toThrow('Invalid webhook signature');
|
||||
});
|
||||
|
||||
it('responds 400 when a correctly signed payload is empty', async () => {
|
||||
it('throws when a correctly signed payload is empty', () => {
|
||||
const rawBody = 'null';
|
||||
|
||||
const result = await recallWebhookRouteHandler(
|
||||
buildRoutePayload({
|
||||
rawBody,
|
||||
body: null,
|
||||
headers: buildSignedHeaders(rawBody),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
__twentyHttpResponse: true,
|
||||
status: 400,
|
||||
body: {
|
||||
error: 'Webhook payload was empty',
|
||||
},
|
||||
});
|
||||
expect(() =>
|
||||
recallWebhookRouteHandler(
|
||||
buildRoutePayload({
|
||||
rawBody,
|
||||
body: null,
|
||||
headers: buildSignedHeaders(rawBody),
|
||||
}),
|
||||
),
|
||||
).toThrow('Webhook payload was empty');
|
||||
});
|
||||
|
||||
it('dispatches a correctly signed payload to the handler', async () => {
|
||||
it('throws when the workspace id is missing from the bot metadata', () => {
|
||||
const body = {
|
||||
event: 'recording.done',
|
||||
data: { bot: { id: 'recall-bot-1' } },
|
||||
};
|
||||
const rawBody = JSON.stringify(body);
|
||||
|
||||
expect(() =>
|
||||
recallWebhookRouteHandler(
|
||||
buildRoutePayload({
|
||||
rawBody,
|
||||
body,
|
||||
headers: buildSignedHeaders(rawBody),
|
||||
}),
|
||||
),
|
||||
).toThrow('workspace id');
|
||||
});
|
||||
|
||||
it('resolves the target workspace for a correctly signed payload', () => {
|
||||
const body = buildRecordingDoneWebhookBody();
|
||||
const rawBody = JSON.stringify(body);
|
||||
|
||||
const result = await recallWebhookRouteHandler(
|
||||
const result = recallWebhookRouteHandler(
|
||||
buildRoutePayload({
|
||||
rawBody,
|
||||
body,
|
||||
@@ -185,10 +170,11 @@ describe('recallWebhookRouteHandler', () => {
|
||||
}),
|
||||
);
|
||||
|
||||
expect(handleRecallWebhookMock).toHaveBeenCalledTimes(1);
|
||||
expect(handleRecallWebhookMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ body }),
|
||||
);
|
||||
expect(result).toEqual({ status: 'updated' });
|
||||
expect(result).toEqual({
|
||||
workspaceId: WORKSPACE_ID,
|
||||
targetLogicFunctionUniversalIdentifier:
|
||||
PROCESS_RECALL_WEBHOOK_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER,
|
||||
payload: body,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+2
-8
@@ -2,16 +2,12 @@ import { type CoreApiClient } from 'twenty-client-sdk/core';
|
||||
|
||||
import { CallRecordingStatus } from 'src/logic-functions/constants/call-recording-status';
|
||||
import { NON_TERMINAL_CALL_RECORDING_STATUSES } from 'src/logic-functions/constants/non-terminal-call-recording-statuses';
|
||||
import {
|
||||
executeCurrentSchemaMutation,
|
||||
type CurrentSchemaUpdateCallRecordingsMutation,
|
||||
} from 'src/logic-functions/data/execute-current-schema-mutation.util';
|
||||
|
||||
export const completeCallRecordingIngestion = async (
|
||||
client: CoreApiClient,
|
||||
{ id }: { id: string },
|
||||
): Promise<boolean> => {
|
||||
const mutation = {
|
||||
const result = await client.mutation({
|
||||
updateCallRecordings: {
|
||||
__args: {
|
||||
filter: {
|
||||
@@ -22,9 +18,7 @@ export const completeCallRecordingIngestion = async (
|
||||
},
|
||||
id: true,
|
||||
},
|
||||
} satisfies CurrentSchemaUpdateCallRecordingsMutation;
|
||||
|
||||
const result = await executeCurrentSchemaMutation(client, mutation);
|
||||
});
|
||||
|
||||
return (result.updateCallRecordings ?? []).length > 0;
|
||||
};
|
||||
|
||||
-54
@@ -1,54 +0,0 @@
|
||||
import { type CoreApiClient } from 'twenty-client-sdk/core';
|
||||
|
||||
import { type CallRecordingStatus } from 'src/logic-functions/constants/call-recording-status';
|
||||
import { type CallRecordingUpdateFields } from 'src/logic-functions/data/update-call-recording.util';
|
||||
|
||||
type CurrentSchemaMutationFunction = (
|
||||
mutation: CurrentSchemaMutation,
|
||||
) => Promise<CurrentSchemaMutationResult>;
|
||||
|
||||
export type CurrentSchemaUpdateCallRecordingMutation = {
|
||||
updateCallRecording: {
|
||||
__args: {
|
||||
id: string;
|
||||
data: CallRecordingUpdateFields;
|
||||
};
|
||||
id?: true;
|
||||
status?: true;
|
||||
};
|
||||
};
|
||||
|
||||
export type CurrentSchemaUpdateCallRecordingsMutation = {
|
||||
updateCallRecordings: {
|
||||
__args: {
|
||||
filter: {
|
||||
id: { eq: string };
|
||||
status?: { in: CallRecordingStatus[] };
|
||||
};
|
||||
data: Pick<CallRecordingUpdateFields, 'status'>;
|
||||
};
|
||||
id?: true;
|
||||
};
|
||||
};
|
||||
|
||||
type CurrentSchemaMutation =
|
||||
| CurrentSchemaUpdateCallRecordingMutation
|
||||
| CurrentSchemaUpdateCallRecordingsMutation;
|
||||
|
||||
type CurrentSchemaMutationResult = {
|
||||
updateCallRecording?: { id?: string; status?: string | null } | null;
|
||||
updateCallRecordings?: { id?: string }[] | null;
|
||||
};
|
||||
|
||||
// TODO: Remove this bridge once the released SDK includes the current
|
||||
// CallRecording schema with FAILED and callRecorderFailureReason.
|
||||
export const executeCurrentSchemaMutation = (
|
||||
client: CoreApiClient,
|
||||
mutation: CurrentSchemaMutation,
|
||||
): Promise<CurrentSchemaMutationResult> => {
|
||||
const currentSchemaClient = client as {
|
||||
mutation: CurrentSchemaMutationFunction;
|
||||
};
|
||||
|
||||
return currentSchemaClient.mutation(mutation);
|
||||
};
|
||||
+3
-27
@@ -1,28 +1,6 @@
|
||||
import { type CoreApiClient } from 'twenty-client-sdk/core';
|
||||
|
||||
import { type CallRecordingRequestStatus } from 'src/logic-functions/constants/call-recording-request-status';
|
||||
import { type CallRecordingStatus } from 'src/logic-functions/constants/call-recording-status';
|
||||
import {
|
||||
executeCurrentSchemaMutation,
|
||||
type CurrentSchemaUpdateCallRecordingMutation,
|
||||
} from 'src/logic-functions/data/execute-current-schema-mutation.util';
|
||||
|
||||
export type CallRecordingUpdateFields = Partial<{
|
||||
// null clears a previously synced title when the calendar title disappears.
|
||||
title: string | null;
|
||||
status: CallRecordingStatus;
|
||||
recordingRequestStatus: CallRecordingRequestStatus;
|
||||
startedAt: string;
|
||||
endedAt: string;
|
||||
calendarEventId: string;
|
||||
// null clears stale app-owned state on cancel/eject or reschedule.
|
||||
externalBotId: string | null;
|
||||
externalRecordingId: string;
|
||||
callRecorderFailureReason: string | null;
|
||||
transcript: Record<string, unknown>;
|
||||
audio: { fileId: string; label: string }[];
|
||||
video: { fileId: string; label: string }[];
|
||||
}>;
|
||||
import { type CallRecordingUpdateFields } from 'src/logic-functions/types/call-recording-update-fields.type';
|
||||
|
||||
export const updateCallRecording = async (
|
||||
client: CoreApiClient,
|
||||
@@ -34,7 +12,7 @@ export const updateCallRecording = async (
|
||||
data: CallRecordingUpdateFields;
|
||||
},
|
||||
): Promise<void> => {
|
||||
const mutation = {
|
||||
await client.mutation({
|
||||
updateCallRecording: {
|
||||
__args: {
|
||||
id,
|
||||
@@ -42,7 +20,5 @@ export const updateCallRecording = async (
|
||||
},
|
||||
id: true,
|
||||
},
|
||||
} satisfies CurrentSchemaUpdateCallRecordingMutation;
|
||||
|
||||
await executeCurrentSchemaMutation(client, mutation);
|
||||
});
|
||||
};
|
||||
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
import { type MeetingRecording } from 'src/logic-functions/types/meeting-recording.type';
|
||||
import { type RecallBotMetadata } from 'src/logic-functions/types/recall-bot-metadata.type';
|
||||
import { computeRealMeetingKey } from 'src/logic-functions/domain/compute-real-meeting-key.util';
|
||||
|
||||
export const buildRecallBotMetadata = ({
|
||||
callRecording,
|
||||
calendarEvent,
|
||||
workspaceId,
|
||||
}: MeetingRecording & { workspaceId: string }): RecallBotMetadata => {
|
||||
return {
|
||||
twentyWorkspaceId: workspaceId,
|
||||
twentyCallRecordingId: callRecording.id,
|
||||
twentyCalendarEventId: calendarEvent.id,
|
||||
twentyRealMeetingKey: computeRealMeetingKey({
|
||||
calendarEventId: calendarEvent.id,
|
||||
conferenceLinkUrl: calendarEvent.conferenceLinkUrl,
|
||||
iCalUid: calendarEvent.iCalUid,
|
||||
startsAt: calendarEvent.startsAt,
|
||||
}),
|
||||
};
|
||||
};
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { type RecallRoutingMetadata } from 'src/logic-functions/types/recall-routing-metadata.type';
|
||||
|
||||
export const buildRecallRoutingMetadata = ({
|
||||
callRecordingId,
|
||||
workspaceId,
|
||||
}: {
|
||||
callRecordingId: string;
|
||||
workspaceId: string;
|
||||
}): RecallRoutingMetadata => ({
|
||||
twentyWorkspaceId: workspaceId,
|
||||
twentyCallRecordingId: callRecordingId,
|
||||
});
|
||||
+1
-1
@@ -2,7 +2,7 @@ import { CallRecordingStatus } from 'src/logic-functions/constants/call-recordin
|
||||
import { type FilesFieldValue } from 'src/logic-functions/types/files-field-value.type';
|
||||
import { computeCallRecordingCharge } from 'src/logic-functions/domain/compute-call-recording-charge.util';
|
||||
import { isCallRecordingIngestionComplete } from 'src/logic-functions/domain/is-call-recording-ingestion-complete.util';
|
||||
import { type CallRecordingUpdateFields } from 'src/logic-functions/data/update-call-recording.util';
|
||||
import { type CallRecordingUpdateFields } from 'src/logic-functions/types/call-recording-update-fields.type';
|
||||
|
||||
export const shouldCompleteCallRecordingIngestion = ({
|
||||
current,
|
||||
|
||||
-2
@@ -160,7 +160,6 @@ describe('convergeDivergedCallRecordings', () => {
|
||||
});
|
||||
expect(createAsyncRecallTranscriptMock).toHaveBeenCalledWith({
|
||||
externalRecordingId: 'recall-recording-1',
|
||||
callRecordingId: 'call-recording-1',
|
||||
});
|
||||
expect(client.mutations).toEqual([
|
||||
expect.objectContaining({
|
||||
@@ -497,7 +496,6 @@ describe('convergeDivergedCallRecordings', () => {
|
||||
expect(createAsyncRecallTranscriptMock).toHaveBeenCalledTimes(1);
|
||||
expect(createAsyncRecallTranscriptMock).toHaveBeenCalledWith({
|
||||
externalRecordingId: 'recall-recording-1',
|
||||
callRecordingId: 'call-recording-1',
|
||||
});
|
||||
expect(client.mutations).toEqual([
|
||||
{
|
||||
|
||||
+18
-3
@@ -12,6 +12,7 @@ const buildRecordingDoneWebhookBody = () => ({
|
||||
id: 'recall-bot-1',
|
||||
metadata: {
|
||||
twentyWorkspaceId: WORKSPACE_ID,
|
||||
twentyCallRecordingId: 'call-recording-1',
|
||||
},
|
||||
},
|
||||
recording: {
|
||||
@@ -190,6 +191,7 @@ describe('handleRecallWebhook', () => {
|
||||
bot: {
|
||||
id: 'recall-bot-1',
|
||||
metadata: {
|
||||
twentyWorkspaceId: WORKSPACE_ID,
|
||||
twentyCallRecordingId: 'call-recording-1',
|
||||
},
|
||||
},
|
||||
@@ -237,6 +239,7 @@ describe('handleRecallWebhook', () => {
|
||||
bot: {
|
||||
id: 'recall-bot-1',
|
||||
metadata: {
|
||||
twentyWorkspaceId: WORKSPACE_ID,
|
||||
twentyCallRecordingId: 'call-recording-1',
|
||||
},
|
||||
},
|
||||
@@ -272,6 +275,7 @@ describe('handleRecallWebhook', () => {
|
||||
bot: {
|
||||
id: 'recall-bot-1',
|
||||
metadata: {
|
||||
twentyWorkspaceId: WORKSPACE_ID,
|
||||
twentyCallRecordingId: 'call-recording-1',
|
||||
},
|
||||
},
|
||||
@@ -321,6 +325,7 @@ describe('handleRecallWebhook', () => {
|
||||
bot: {
|
||||
id: 'recall-bot-1',
|
||||
metadata: {
|
||||
twentyWorkspaceId: WORKSPACE_ID,
|
||||
twentyCallRecordingId: 'call-recording-current',
|
||||
},
|
||||
},
|
||||
@@ -410,6 +415,7 @@ describe('handleRecallWebhook', () => {
|
||||
bot: {
|
||||
id: 'recall-bot-1',
|
||||
metadata: {
|
||||
twentyWorkspaceId: WORKSPACE_ID,
|
||||
twentyCallRecordingId: 'call-recording-1',
|
||||
},
|
||||
},
|
||||
@@ -457,6 +463,7 @@ describe('handleRecallWebhook', () => {
|
||||
bot: {
|
||||
id: 'recall-bot-1',
|
||||
metadata: {
|
||||
twentyWorkspaceId: WORKSPACE_ID,
|
||||
twentyCallRecordingId: 'call-recording-1',
|
||||
},
|
||||
},
|
||||
@@ -504,6 +511,7 @@ describe('handleRecallWebhook', () => {
|
||||
bot: {
|
||||
id: 'recall-bot-1',
|
||||
metadata: {
|
||||
twentyWorkspaceId: WORKSPACE_ID,
|
||||
twentyCallRecordingId: 'call-recording-1',
|
||||
},
|
||||
},
|
||||
@@ -545,6 +553,7 @@ describe('handleRecallWebhook', () => {
|
||||
bot: {
|
||||
id: 'recall-bot-1',
|
||||
metadata: {
|
||||
twentyWorkspaceId: WORKSPACE_ID,
|
||||
twentyCallRecordingId: 'call-recording-1',
|
||||
},
|
||||
},
|
||||
@@ -591,6 +600,7 @@ describe('handleRecallWebhook', () => {
|
||||
bot: {
|
||||
id: 'recall-bot-1',
|
||||
metadata: {
|
||||
twentyWorkspaceId: WORKSPACE_ID,
|
||||
twentyCallRecordingId: 'call-recording-1',
|
||||
},
|
||||
},
|
||||
@@ -632,6 +642,7 @@ describe('handleRecallWebhook', () => {
|
||||
bot: {
|
||||
id: 'recall-bot-1',
|
||||
metadata: {
|
||||
twentyWorkspaceId: WORKSPACE_ID,
|
||||
twentyCallRecordingId: 'call-recording-1',
|
||||
},
|
||||
},
|
||||
@@ -668,6 +679,7 @@ describe('handleRecallWebhook', () => {
|
||||
bot: {
|
||||
id: 'recall-bot-1',
|
||||
metadata: {
|
||||
twentyWorkspaceId: WORKSPACE_ID,
|
||||
twentyCallRecordingId: 'call-recording-1',
|
||||
},
|
||||
},
|
||||
@@ -696,6 +708,7 @@ describe('handleRecallWebhook', () => {
|
||||
data: {
|
||||
bot: {
|
||||
metadata: {
|
||||
twentyWorkspaceId: WORKSPACE_ID,
|
||||
twentyCallRecordingId: 'call-recording-deleted',
|
||||
},
|
||||
},
|
||||
@@ -755,7 +768,6 @@ describe('handleRecallWebhook', () => {
|
||||
expect(createAsyncRecallTranscriptMock).toHaveBeenCalledTimes(1);
|
||||
expect(createAsyncRecallTranscriptMock).toHaveBeenCalledWith({
|
||||
externalRecordingId: 'recall-recording-1',
|
||||
callRecordingId: 'call-recording-1',
|
||||
});
|
||||
expect(client.mutations).toEqual([
|
||||
{
|
||||
@@ -841,6 +853,7 @@ describe('handleRecallWebhook', () => {
|
||||
bot: {
|
||||
id: 'recall-bot-1',
|
||||
metadata: {
|
||||
twentyWorkspaceId: WORKSPACE_ID,
|
||||
twentyCallRecordingId: 'call-recording-1',
|
||||
},
|
||||
},
|
||||
@@ -856,7 +869,6 @@ describe('handleRecallWebhook', () => {
|
||||
});
|
||||
expect(createAsyncRecallTranscriptMock).toHaveBeenCalledWith({
|
||||
externalRecordingId: 'recall-recording-9',
|
||||
callRecordingId: 'call-recording-1',
|
||||
});
|
||||
expect(client.mutations).toEqual([
|
||||
expect.objectContaining({
|
||||
@@ -954,7 +966,6 @@ describe('handleRecallWebhook', () => {
|
||||
|
||||
expect(createAsyncRecallTranscriptMock).toHaveBeenCalledWith({
|
||||
externalRecordingId: 'recall-recording-1',
|
||||
callRecordingId: 'call-recording-1',
|
||||
});
|
||||
expect(client.mutations).toEqual([
|
||||
expect.objectContaining({
|
||||
@@ -1070,6 +1081,7 @@ describe('handleRecallWebhook', () => {
|
||||
bot: {
|
||||
id: 'recall-bot-1',
|
||||
metadata: {
|
||||
twentyWorkspaceId: WORKSPACE_ID,
|
||||
twentyCallRecordingId: 'call-recording-1',
|
||||
},
|
||||
},
|
||||
@@ -1150,6 +1162,7 @@ describe('handleRecallWebhook', () => {
|
||||
bot: {
|
||||
id: 'recall-bot-1',
|
||||
metadata: {
|
||||
twentyWorkspaceId: WORKSPACE_ID,
|
||||
twentyCallRecordingId: 'call-recording-1',
|
||||
},
|
||||
},
|
||||
@@ -1209,6 +1222,7 @@ describe('handleRecallWebhook', () => {
|
||||
bot: {
|
||||
id: 'recall-bot-1',
|
||||
metadata: {
|
||||
twentyWorkspaceId: WORKSPACE_ID,
|
||||
twentyCallRecordingId: 'call-recording-1',
|
||||
},
|
||||
},
|
||||
@@ -1263,6 +1277,7 @@ describe('handleRecallWebhook', () => {
|
||||
bot: {
|
||||
id: 'recall-bot-1',
|
||||
metadata: {
|
||||
twentyWorkspaceId: WORKSPACE_ID,
|
||||
twentyCallRecordingId: 'call-recording-1',
|
||||
},
|
||||
},
|
||||
|
||||
-6
@@ -265,9 +265,6 @@ describe('reconcileCallRecorderForCalendarEventIds', () => {
|
||||
metadata: {
|
||||
twentyWorkspaceId: WORKSPACE_ID,
|
||||
twentyCallRecordingId: buildCustomerSyncCallRecordingId(),
|
||||
twentyCalendarEventId: 'calendar-event-1',
|
||||
twentyRealMeetingKey:
|
||||
'link:meet.example.com/customer-sync:2026-01-01T13:00:00.000Z',
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -416,9 +413,6 @@ describe('reconcileCallRecorderForCalendarEventIds', () => {
|
||||
metadata: {
|
||||
twentyWorkspaceId: WORKSPACE_ID,
|
||||
twentyCallRecordingId: buildCustomerSyncCallRecordingId(),
|
||||
twentyCalendarEventId: 'calendar-event-1',
|
||||
twentyRealMeetingKey:
|
||||
'link:meet.example.com/customer-sync:2026-01-01T13:00:00.000Z',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
+2
-4
@@ -23,10 +23,8 @@ import { persistCallRecordingProgress } from 'src/logic-functions/flows/persist-
|
||||
import { reconcileCallRecordingTranscriptArtifact } from 'src/logic-functions/flows/reconcile-call-recording-transcript-artifact.util';
|
||||
import { type ConvergeDivergedCallRecordingsResult } from 'src/logic-functions/flows/converge-diverged-call-recordings-result.type';
|
||||
import { shouldCompleteCallRecordingIngestion } from 'src/logic-functions/domain/should-complete-call-recording-ingestion.util';
|
||||
import {
|
||||
updateCallRecording,
|
||||
type CallRecordingUpdateFields,
|
||||
} from 'src/logic-functions/data/update-call-recording.util';
|
||||
import { updateCallRecording } from 'src/logic-functions/data/update-call-recording.util';
|
||||
import { type CallRecordingUpdateFields } from 'src/logic-functions/types/call-recording-update-fields.type';
|
||||
|
||||
const CONVERGENCE_LOOKBACK_DAYS = 7;
|
||||
|
||||
|
||||
+3
-4
@@ -3,7 +3,7 @@ import { type CoreApiClient } from 'twenty-client-sdk/core';
|
||||
|
||||
import { CallRecordingRequestStatus } from 'src/logic-functions/constants/call-recording-request-status';
|
||||
import { type MeetingRecording } from 'src/logic-functions/types/meeting-recording.type';
|
||||
import { buildRecallBotMetadata } from 'src/logic-functions/domain/build-recall-bot-metadata.util';
|
||||
import { buildRecallRoutingMetadata } from 'src/logic-functions/domain/build-recall-routing-metadata.util';
|
||||
import { computeRecallBotJoinAt } from 'src/logic-functions/domain/compute-recall-bot-join-at.util';
|
||||
import { findCallRecordingsByIds } from 'src/logic-functions/data/find-call-recordings-by-ids.util';
|
||||
import { getCurrentWorkspaceId } from 'src/logic-functions/data/get-current-workspace-id.util';
|
||||
@@ -50,9 +50,8 @@ export const ensureCallRecorder = async (
|
||||
const scheduleResult = await scheduleRecallBot({
|
||||
meetingUrl,
|
||||
joinAt,
|
||||
metadata: buildRecallBotMetadata({
|
||||
callRecording,
|
||||
calendarEvent,
|
||||
metadata: buildRecallRoutingMetadata({
|
||||
callRecordingId: callRecording.id,
|
||||
workspaceId,
|
||||
}),
|
||||
});
|
||||
|
||||
+2
-4
@@ -21,10 +21,8 @@ import {
|
||||
import { parseTranscriptMarker } from 'src/logic-functions/domain/parse-transcript-marker.util';
|
||||
import { persistCallRecordingProgress } from 'src/logic-functions/flows/persist-call-recording-progress.util';
|
||||
import { reconcileCallRecordingTranscriptArtifact } from 'src/logic-functions/flows/reconcile-call-recording-transcript-artifact.util';
|
||||
import {
|
||||
updateCallRecording,
|
||||
type CallRecordingUpdateFields,
|
||||
} from 'src/logic-functions/data/update-call-recording.util';
|
||||
import { updateCallRecording } from 'src/logic-functions/data/update-call-recording.util';
|
||||
import { type CallRecordingUpdateFields } from 'src/logic-functions/types/call-recording-update-fields.type';
|
||||
|
||||
type MatchedCallRecording = {
|
||||
id: string;
|
||||
|
||||
+3
-2
@@ -5,7 +5,8 @@ import { CALL_RECORDING_AUDIO_FIELD_UNIVERSAL_IDENTIFIER } from 'src/constants/c
|
||||
import { CALL_RECORDING_VIDEO_FIELD_UNIVERSAL_IDENTIFIER } from 'src/constants/call-recording-video-field-universal-identifier';
|
||||
import { extractRecallMediaUrls } from 'src/logic-functions/recall-api/extract-recall-media-urls.util';
|
||||
import { getRecallRecording } from 'src/logic-functions/recall-api/get-recall-recording.util';
|
||||
import { type CallRecordingUpdateFields } from 'src/logic-functions/data/update-call-recording.util';
|
||||
import { type CallRecordingMediaFile } from 'src/logic-functions/types/call-recording-media-file.type';
|
||||
import { type CallRecordingUpdateFields } from 'src/logic-functions/types/call-recording-update-fields.type';
|
||||
|
||||
type CallRecordingMediaUpdateFields = Pick<
|
||||
CallRecordingUpdateFields,
|
||||
@@ -88,7 +89,7 @@ const ingestMediaArtifact = async ({
|
||||
url: string;
|
||||
fileName: string;
|
||||
fieldMetadataUniversalIdentifier: string;
|
||||
}): Promise<{ fileId: string; label: string }[] | undefined> => {
|
||||
}): Promise<CallRecordingMediaFile[] | undefined> => {
|
||||
try {
|
||||
const { buffer, contentType } = await downloadMediaFile(url);
|
||||
const uploadedFile = await metadataClient.uploadFile(
|
||||
|
||||
+2
-4
@@ -3,10 +3,8 @@ import { type CoreApiClient } from 'twenty-client-sdk/core';
|
||||
import { type FilesFieldValue } from 'src/logic-functions/types/files-field-value.type';
|
||||
import { completeAndChargeCallRecording } from 'src/logic-functions/flows/complete-and-charge-call-recording.util';
|
||||
import { shouldCompleteCallRecordingIngestion } from 'src/logic-functions/domain/should-complete-call-recording-ingestion.util';
|
||||
import {
|
||||
updateCallRecording,
|
||||
type CallRecordingUpdateFields,
|
||||
} from 'src/logic-functions/data/update-call-recording.util';
|
||||
import { updateCallRecording } from 'src/logic-functions/data/update-call-recording.util';
|
||||
import { type CallRecordingUpdateFields } from 'src/logic-functions/types/call-recording-update-fields.type';
|
||||
|
||||
type PersistCallRecordingProgressCurrent = {
|
||||
status?: string;
|
||||
|
||||
+2
-4
@@ -23,10 +23,8 @@ import { findCallRecordingsByCalendarEventIds } from 'src/logic-functions/data/f
|
||||
import { findCallRecordingsByIds } from 'src/logic-functions/data/find-call-recordings-by-ids.util';
|
||||
import { getUniqueSortedIds } from 'src/logic-functions/utils/get-unique-sorted-ids.util';
|
||||
import { rescheduleCallRecordingBot } from 'src/logic-functions/flows/reschedule-call-recording-bot.util';
|
||||
import {
|
||||
updateCallRecording,
|
||||
type CallRecordingUpdateFields,
|
||||
} from 'src/logic-functions/data/update-call-recording.util';
|
||||
import { updateCallRecording } from 'src/logic-functions/data/update-call-recording.util';
|
||||
import { type CallRecordingUpdateFields } from 'src/logic-functions/types/call-recording-update-fields.type';
|
||||
|
||||
export const reconcileCallRecorderForCalendarEventIds = async ({
|
||||
client,
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { type CallRecordingUpdateFields } from 'src/logic-functions/data/update-call-recording.util';
|
||||
import { type CallRecordingUpdateFields } from 'src/logic-functions/types/call-recording-update-fields.type';
|
||||
|
||||
type CallRecordingTranscriptArtifactUpdateFields = Pick<
|
||||
CallRecordingUpdateFields,
|
||||
|
||||
-1
@@ -68,7 +68,6 @@ export const reconcileCallRecordingTranscriptArtifact = async ({
|
||||
) {
|
||||
const createResult = await createAsyncRecallTranscript({
|
||||
externalRecordingId,
|
||||
callRecordingId,
|
||||
});
|
||||
|
||||
if (!createResult.ok) {
|
||||
|
||||
+3
-4
@@ -2,7 +2,7 @@ import { isUndefined } from '@sniptt/guards';
|
||||
import { type CoreApiClient } from 'twenty-client-sdk/core';
|
||||
|
||||
import { type MeetingRecording } from 'src/logic-functions/types/meeting-recording.type';
|
||||
import { buildRecallBotMetadata } from 'src/logic-functions/domain/build-recall-bot-metadata.util';
|
||||
import { buildRecallRoutingMetadata } from 'src/logic-functions/domain/build-recall-routing-metadata.util';
|
||||
import { computeRecallBotJoinAt } from 'src/logic-functions/domain/compute-recall-bot-join-at.util';
|
||||
import { getCurrentWorkspaceId } from 'src/logic-functions/data/get-current-workspace-id.util';
|
||||
import { rescheduleRecallBot } from 'src/logic-functions/recall-api/reschedule-recall-bot.util';
|
||||
@@ -43,9 +43,8 @@ export const rescheduleCallRecordingBot = async (
|
||||
externalBotId,
|
||||
meetingUrl,
|
||||
joinAt,
|
||||
metadata: buildRecallBotMetadata({
|
||||
callRecording,
|
||||
calendarEvent,
|
||||
metadata: buildRecallRoutingMetadata({
|
||||
callRecordingId: callRecording.id,
|
||||
workspaceId,
|
||||
}),
|
||||
});
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
|
||||
import { PROCESS_RECALL_WEBHOOK_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER } from 'src/constants/process-recall-webhook-logic-function-universal-identifier';
|
||||
import { handleRecallWebhook } from 'src/logic-functions/flows/handle-recall-webhook.util';
|
||||
import { type RecallWebhookBody } from 'src/logic-functions/recall-api/parse-recall-webhook-event.util';
|
||||
|
||||
// Dispatched by the recall-webhook resolver; runs in the resolved workspace so the client is workspace-scoped.
|
||||
export const processRecallWebhookHandler = (body: RecallWebhookBody) =>
|
||||
handleRecallWebhook({
|
||||
client: new CoreApiClient(),
|
||||
body,
|
||||
});
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier:
|
||||
PROCESS_RECALL_WEBHOOK_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER,
|
||||
name: 'process-recall-webhook',
|
||||
description:
|
||||
'Updates the matching CallRecording lifecycle status from a verified Recall.ai webhook event.',
|
||||
timeoutSeconds: 30,
|
||||
handler: processRecallWebhookHandler,
|
||||
});
|
||||
+11
-62
@@ -13,6 +13,10 @@ import { CALL_RECORDER_RECORDING_RETENTION_HOURS_ENV_VAR_NAME } from 'src/logic-
|
||||
|
||||
const getRecallApiConfigMock = vi.hoisted(() => vi.fn());
|
||||
const WORKSPACE_ID = '123e4567-e89b-12d3-a456-426614174000';
|
||||
const RECALL_ROUTING_METADATA = {
|
||||
twentyWorkspaceId: WORKSPACE_ID,
|
||||
twentyCallRecordingId: 'call-recording-id',
|
||||
};
|
||||
|
||||
vi.mock('src/logic-functions/recall-api/get-recall-api-config.util', () => ({
|
||||
getRecallApiConfig: getRecallApiConfigMock,
|
||||
@@ -45,12 +49,7 @@ describe('recall bot api', () => {
|
||||
const result = await scheduleRecallBot({
|
||||
meetingUrl: 'https://meet.google.com/abc-defg-hij',
|
||||
joinAt: '2026-01-01T13:00:00.000Z',
|
||||
metadata: {
|
||||
twentyWorkspaceId: WORKSPACE_ID,
|
||||
twentyCallRecordingId: 'call-recording-id',
|
||||
twentyCalendarEventId: 'calendar-event-id',
|
||||
twentyRealMeetingKey: 'meeting-key',
|
||||
},
|
||||
metadata: RECALL_ROUTING_METADATA,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ ok: true, externalBotId: 'recall-bot-id' });
|
||||
@@ -73,12 +72,7 @@ describe('recall bot api', () => {
|
||||
audio_mixed_mp3: {},
|
||||
retention: { type: 'timed', hours: 166 },
|
||||
},
|
||||
metadata: {
|
||||
twentyWorkspaceId: WORKSPACE_ID,
|
||||
twentyCallRecordingId: 'call-recording-id',
|
||||
twentyCalendarEventId: 'calendar-event-id',
|
||||
twentyRealMeetingKey: 'meeting-key',
|
||||
},
|
||||
metadata: RECALL_ROUTING_METADATA,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -88,12 +82,7 @@ describe('recall bot api', () => {
|
||||
const result = await scheduleRecallBot({
|
||||
meetingUrl: 'https://meet.google.com/abc-defg-hij',
|
||||
joinAt: '2026-01-01T13:00:00.000Z',
|
||||
metadata: {
|
||||
twentyWorkspaceId: WORKSPACE_ID,
|
||||
twentyCallRecordingId: 'call-recording-id',
|
||||
twentyCalendarEventId: 'calendar-event-id',
|
||||
twentyRealMeetingKey: 'meeting-key',
|
||||
},
|
||||
metadata: RECALL_ROUTING_METADATA,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ ok: true, externalBotId: 'recall-bot-id' });
|
||||
@@ -113,12 +102,7 @@ describe('recall bot api', () => {
|
||||
const result = await scheduleRecallBot({
|
||||
meetingUrl: 'https://meet.google.com/abc-defg-hij',
|
||||
joinAt: '2026-01-01T13:00:00.000Z',
|
||||
metadata: {
|
||||
twentyWorkspaceId: WORKSPACE_ID,
|
||||
twentyCallRecordingId: 'call-recording-id',
|
||||
twentyCalendarEventId: 'calendar-event-id',
|
||||
twentyRealMeetingKey: 'meeting-key',
|
||||
},
|
||||
metadata: RECALL_ROUTING_METADATA,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ ok: true, externalBotId: 'recall-bot-id' });
|
||||
@@ -141,12 +125,7 @@ describe('recall bot api', () => {
|
||||
const result = await scheduleRecallBot({
|
||||
meetingUrl: 'https://meet.google.com/abc-defg-hij',
|
||||
joinAt: '2026-01-01T13:00:00.000Z',
|
||||
metadata: {
|
||||
twentyWorkspaceId: WORKSPACE_ID,
|
||||
twentyCallRecordingId: 'call-recording-id',
|
||||
twentyCalendarEventId: 'calendar-event-id',
|
||||
twentyRealMeetingKey: 'meeting-key',
|
||||
},
|
||||
metadata: RECALL_ROUTING_METADATA,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
@@ -168,12 +147,7 @@ describe('recall bot api', () => {
|
||||
externalBotId: 'recall-bot-gone',
|
||||
meetingUrl: 'https://meet.google.com/abc-defg-hij',
|
||||
joinAt: '2026-01-01T13:00:00.000Z',
|
||||
metadata: {
|
||||
twentyWorkspaceId: WORKSPACE_ID,
|
||||
twentyCallRecordingId: 'call-recording-id',
|
||||
twentyCalendarEventId: 'calendar-event-id',
|
||||
twentyRealMeetingKey: 'meeting-key',
|
||||
},
|
||||
metadata: RECALL_ROUTING_METADATA,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
@@ -204,12 +178,7 @@ describe('recall bot api', () => {
|
||||
await scheduleRecallBot({
|
||||
meetingUrl: 'https://meet.google.com/abc-defg-hij',
|
||||
joinAt: '2026-01-01T13:00:00.000Z',
|
||||
metadata: {
|
||||
twentyWorkspaceId: WORKSPACE_ID,
|
||||
twentyCallRecordingId: 'call-recording-id',
|
||||
twentyCalendarEventId: 'calendar-event-id',
|
||||
twentyRealMeetingKey: 'meeting-key',
|
||||
},
|
||||
metadata: RECALL_ROUTING_METADATA,
|
||||
});
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
@@ -522,26 +491,6 @@ describe('recall bot api', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('adds call recording metadata when convergence creates an async transcript', async () => {
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: true,
|
||||
status: 201,
|
||||
json: async () => ({ id: 'recall-transcript-id' }),
|
||||
});
|
||||
|
||||
const result = await createAsyncRecallTranscript({
|
||||
externalRecordingId: 'recall-recording-id',
|
||||
callRecordingId: 'call-recording-id',
|
||||
});
|
||||
|
||||
expect(result).toEqual({ ok: true, transcriptId: 'recall-transcript-id' });
|
||||
expect(JSON.parse(fetchMock.mock.calls[0][1].body)).toEqual({
|
||||
provider: { recallai_async: { language_code: 'auto' } },
|
||||
diarization: { use_separate_streams_when_available: true },
|
||||
metadata: { twentyCallRecordingId: 'call-recording-id' },
|
||||
});
|
||||
});
|
||||
|
||||
it('does not retry async transcript creation failures', async () => {
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: false,
|
||||
|
||||
-5
@@ -10,10 +10,8 @@ type CreateAsyncRecallTranscriptResult =
|
||||
|
||||
export const createAsyncRecallTranscript = async ({
|
||||
externalRecordingId,
|
||||
callRecordingId,
|
||||
}: {
|
||||
externalRecordingId: string;
|
||||
callRecordingId?: string;
|
||||
}): Promise<CreateAsyncRecallTranscriptResult> => {
|
||||
const configResult = getRecallApiConfig();
|
||||
|
||||
@@ -28,9 +26,6 @@ export const createAsyncRecallTranscript = async ({
|
||||
body: {
|
||||
provider: { recallai_async: { language_code: 'auto' } },
|
||||
diarization: { use_separate_streams_when_available: true },
|
||||
...(callRecordingId === undefined
|
||||
? {}
|
||||
: { metadata: { twentyCallRecordingId: callRecordingId } }),
|
||||
},
|
||||
maxAttempts: 1,
|
||||
});
|
||||
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
import { getRecallWebhookBotMetadata } from 'src/logic-functions/recall-api/get-recall-webhook-bot-metadata.util';
|
||||
import { type RecallWebhookBody } from 'src/logic-functions/recall-api/parse-recall-webhook-event.util';
|
||||
import { getString } from 'src/logic-functions/utils/get-string.util';
|
||||
|
||||
export const extractTwentyWorkspaceIdFromRecallWebhook = (
|
||||
body: RecallWebhookBody,
|
||||
): string | undefined =>
|
||||
getString(getRecallWebhookBotMetadata(body)?.twentyWorkspaceId);
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { type RecallWebhookBody } from 'src/logic-functions/recall-api/parse-recall-webhook-event.util';
|
||||
import { asRecord } from 'src/logic-functions/utils/as-record.util';
|
||||
import { getRecordAtPath } from 'src/logic-functions/utils/get-record-at-path.util';
|
||||
|
||||
// Recall delivers bot metadata under several body shapes per event family; this is the single reader of all of them.
|
||||
export const getRecallWebhookBotMetadata = (
|
||||
body: RecallWebhookBody,
|
||||
): Record<string, unknown> | undefined => {
|
||||
const data = asRecord(body.data);
|
||||
const bot = asRecord(body.bot);
|
||||
|
||||
return (
|
||||
asRecord(bot?.metadata) ??
|
||||
asRecord(getRecordAtPath(data, ['bot', 'metadata'])) ??
|
||||
asRecord(getRecordAtPath(data, ['recording', 'metadata'])) ??
|
||||
asRecord(data?.metadata)
|
||||
);
|
||||
};
|
||||
+4
-20
@@ -1,5 +1,6 @@
|
||||
import { isUndefined } from '@sniptt/guards';
|
||||
|
||||
import { getRecallWebhookBotMetadata } from 'src/logic-functions/recall-api/get-recall-webhook-bot-metadata.util';
|
||||
import { asRecord } from 'src/logic-functions/utils/as-record.util';
|
||||
import { getRecordAtPath } from 'src/logic-functions/utils/get-record-at-path.util';
|
||||
import { getString } from 'src/logic-functions/utils/get-string.util';
|
||||
@@ -60,10 +61,9 @@ export const parseRecallWebhookEvent = (
|
||||
getString(getRecordAtPath(data, ['status', 'recording_id'])) ??
|
||||
getString(getRecordAtPath(data, ['recording', 'id'])) ??
|
||||
getString(data?.recording_id),
|
||||
callRecordingIdFromMetadata: extractCallRecordingIdFromMetadata({
|
||||
data,
|
||||
bot,
|
||||
}),
|
||||
callRecordingIdFromMetadata: getString(
|
||||
getRecallWebhookBotMetadata(body)?.twentyCallRecordingId,
|
||||
),
|
||||
recordingStartedAt: normalizeRecallTimestamp(
|
||||
getString(getRecordAtPath(data, ['recording', 'started_at'])),
|
||||
),
|
||||
@@ -86,19 +86,3 @@ const getStatusCodeFromEventName = (event: string): string | undefined => {
|
||||
|
||||
return statusCode === 'status_change' ? undefined : statusCode;
|
||||
};
|
||||
|
||||
const extractCallRecordingIdFromMetadata = ({
|
||||
data,
|
||||
bot,
|
||||
}: {
|
||||
data: Record<string, unknown> | undefined;
|
||||
bot: Record<string, unknown> | undefined;
|
||||
}): string | undefined => {
|
||||
const metadata =
|
||||
asRecord(bot?.metadata) ??
|
||||
asRecord(getRecordAtPath(data, ['bot', 'metadata'])) ??
|
||||
asRecord(getRecordAtPath(data, ['recording', 'metadata'])) ??
|
||||
asRecord(data?.metadata);
|
||||
|
||||
return getString(metadata?.twentyCallRecordingId);
|
||||
};
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@ import { isUndefined } from '@sniptt/guards';
|
||||
|
||||
import { getRecallBotAutomaticLeave } from 'src/logic-functions/constants/recall-bot-automatic-leave';
|
||||
import { getRecallBotRecordingConfig } from 'src/logic-functions/constants/recall-bot-recording-config';
|
||||
import { type RecallBotMetadata } from 'src/logic-functions/types/recall-bot-metadata.type';
|
||||
import { type RecallRoutingMetadata } from 'src/logic-functions/types/recall-routing-metadata.type';
|
||||
import { type RecallBotScheduleResult } from 'src/logic-functions/types/recall-bot-operation-result.type';
|
||||
import {
|
||||
extractRecallBotId,
|
||||
@@ -14,7 +14,7 @@ import { recallBotApiRequest } from 'src/logic-functions/recall-api/recall-bot-a
|
||||
export type ScheduleRecallBotArgs = {
|
||||
meetingUrl: string;
|
||||
joinAt: string;
|
||||
metadata: RecallBotMetadata;
|
||||
metadata: RecallRoutingMetadata;
|
||||
};
|
||||
|
||||
export const scheduleRecallBot = async ({
|
||||
|
||||
@@ -1,33 +1,31 @@
|
||||
import { isNull, isUndefined } from '@sniptt/guards';
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define';
|
||||
import { Response } from 'twenty-sdk/logic-function';
|
||||
|
||||
import { PROCESS_RECALL_WEBHOOK_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER } from 'src/constants/process-recall-webhook-logic-function-universal-identifier';
|
||||
import { RECALL_WEBHOOK_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER } from 'src/constants/recall-webhook-logic-function-universal-identifier';
|
||||
import { RECALL_WEBHOOK_SECRET_ENV_VAR_NAME } from 'src/logic-functions/constants/recall-webhook-secret-env-var-name';
|
||||
import { handleRecallWebhook } from 'src/logic-functions/flows/handle-recall-webhook.util';
|
||||
import { extractTwentyWorkspaceIdFromRecallWebhook } from 'src/logic-functions/recall-api/extract-twenty-workspace-id-from-recall-webhook.util';
|
||||
import { type RecallWebhookBody } from 'src/logic-functions/recall-api/parse-recall-webhook-event.util';
|
||||
import { verifyRecallWebhookSignature } from 'src/logic-functions/recall-api/verify-recall-webhook-signature.util';
|
||||
import { getApplicationVariableValue } from 'src/logic-functions/utils/get-application-variable-value.util';
|
||||
import { isNonEmptyString } from 'src/logic-functions/utils/is-non-empty-string.util';
|
||||
|
||||
// Non-2xx makes Svix retry; a returned plain object would 200-ack permanently.
|
||||
const rejectWebhook = (status: number, error: string): Response => {
|
||||
console.error(`[call-recorder] webhook rejected: ${error}`);
|
||||
|
||||
return new Response({ error }, { status });
|
||||
type RecallWebhookResolverResult = {
|
||||
workspaceId: string;
|
||||
targetLogicFunctionUniversalIdentifier: string;
|
||||
payload: RecallWebhookBody;
|
||||
};
|
||||
|
||||
export const recallWebhookRouteHandler = async (
|
||||
// A thrown error becomes a non-2xx, which makes Svix retry; a returned result dispatches to the target.
|
||||
export const recallWebhookRouteHandler = (
|
||||
routePayload: RoutePayload<RecallWebhookBody>,
|
||||
): Promise<object> => {
|
||||
): RecallWebhookResolverResult => {
|
||||
const webhookSecret = getApplicationVariableValue(
|
||||
RECALL_WEBHOOK_SECRET_ENV_VAR_NAME,
|
||||
);
|
||||
|
||||
if (!isNonEmptyString(webhookSecret)) {
|
||||
return rejectWebhook(
|
||||
500,
|
||||
throw new Error(
|
||||
'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.',
|
||||
);
|
||||
}
|
||||
@@ -35,8 +33,7 @@ export const recallWebhookRouteHandler = async (
|
||||
const { rawBody } = routePayload;
|
||||
|
||||
if (isUndefined(rawBody)) {
|
||||
return rejectWebhook(
|
||||
500,
|
||||
throw new Error(
|
||||
'Raw request body was not forwarded by the server; cannot verify the webhook signature',
|
||||
);
|
||||
}
|
||||
@@ -48,34 +45,39 @@ export const recallWebhookRouteHandler = async (
|
||||
});
|
||||
|
||||
if (!signatureCheck.valid) {
|
||||
return rejectWebhook(
|
||||
401,
|
||||
`Invalid webhook signature: ${signatureCheck.error}`,
|
||||
throw new Error(`Invalid webhook signature: ${signatureCheck.error}`);
|
||||
}
|
||||
|
||||
const body = routePayload.body;
|
||||
|
||||
if (isUndefined(body) || isNull(body)) {
|
||||
throw new Error('Webhook payload was empty');
|
||||
}
|
||||
|
||||
const workspaceId = extractTwentyWorkspaceIdFromRecallWebhook(body);
|
||||
|
||||
if (!isNonEmptyString(workspaceId)) {
|
||||
throw new Error(
|
||||
'Webhook payload is missing the Twenty workspace id in the Recall bot metadata',
|
||||
);
|
||||
}
|
||||
|
||||
if (isUndefined(routePayload.body) || isNull(routePayload.body)) {
|
||||
return rejectWebhook(400, 'Webhook payload was empty');
|
||||
}
|
||||
|
||||
return handleRecallWebhook({
|
||||
client: new CoreApiClient(),
|
||||
body: routePayload.body,
|
||||
});
|
||||
return {
|
||||
workspaceId,
|
||||
targetLogicFunctionUniversalIdentifier:
|
||||
PROCESS_RECALL_WEBHOOK_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER,
|
||||
payload: body,
|
||||
};
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: RECALL_WEBHOOK_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER,
|
||||
name: 'recall-webhook',
|
||||
description:
|
||||
'Receives Recall.ai webhook events and updates the matching CallRecording lifecycle status.',
|
||||
'Verifies Recall.ai webhook signatures and resolves the target workspace for the matching CallRecording update.',
|
||||
timeoutSeconds: 30,
|
||||
handler: recallWebhookRouteHandler,
|
||||
serverWebhookTriggerSettings: {
|
||||
workspaceIdResolver: {
|
||||
source: 'body',
|
||||
path: 'data.bot.metadata.twentyWorkspaceId',
|
||||
},
|
||||
serverRouteTriggerSettings: {
|
||||
forwardedRequestHeaders: [
|
||||
'webhook-id',
|
||||
'webhook-timestamp',
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
export type CallRecordingMediaFile = { fileId: string; label: string };
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import { type CallRecordingRequestStatus } from 'src/logic-functions/constants/call-recording-request-status';
|
||||
import { type CallRecordingStatus } from 'src/logic-functions/constants/call-recording-status';
|
||||
import { type CallRecordingMediaFile } from 'src/logic-functions/types/call-recording-media-file.type';
|
||||
|
||||
export type CallRecordingUpdateFields = Partial<{
|
||||
// null clears a previously synced title when the calendar title disappears.
|
||||
title: string | null;
|
||||
status: CallRecordingStatus;
|
||||
recordingRequestStatus: CallRecordingRequestStatus;
|
||||
startedAt: string;
|
||||
endedAt: string;
|
||||
calendarEventId: string;
|
||||
// null clears stale app-owned state on cancel/eject or reschedule.
|
||||
externalBotId: string | null;
|
||||
externalRecordingId: string;
|
||||
callRecorderFailureReason: string | null;
|
||||
transcript: Record<string, unknown>;
|
||||
audio: CallRecordingMediaFile[];
|
||||
video: CallRecordingMediaFile[];
|
||||
}>;
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
export type RecallBotMetadata = {
|
||||
twentyWorkspaceId: string;
|
||||
twentyCallRecordingId: string;
|
||||
twentyCalendarEventId: string;
|
||||
twentyRealMeetingKey: string;
|
||||
};
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
export type RecallRoutingMetadata = {
|
||||
twentyWorkspaceId: string;
|
||||
twentyCallRecordingId: string;
|
||||
};
|
||||
@@ -143,6 +143,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@emnapi/runtime@npm:^1.7.0":
|
||||
version: 1.11.1
|
||||
resolution: "@emnapi/runtime@npm:1.11.1"
|
||||
dependencies:
|
||||
tslib: "npm:^2.4.0"
|
||||
checksum: 10c0/04332fb62076afc440aa23316c04bec42f584ca8b074e5507d08e2b33a47cbe0493b1aadb8f3c1057b64ae1e17f5bde1a7bc37f7facc9d0bc25c18197cbd366f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@emnapi/wasi-threads@npm:1.2.1":
|
||||
version: 1.2.1
|
||||
resolution: "@emnapi/wasi-threads@npm:1.2.1"
|
||||
@@ -500,6 +509,233 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/colour@npm:^1.0.0":
|
||||
version: 1.1.0
|
||||
resolution: "@img/colour@npm:1.1.0"
|
||||
checksum: 10c0/2ebea2c0bbaee73b99badcefa04e1e71d83f36e5369337d3121dca841f4569533c4e2faddda6d62dd247f0d5cca143711f9446c59bcce81e427ba433a7a94a17
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-darwin-arm64@npm:0.34.5":
|
||||
version: 0.34.5
|
||||
resolution: "@img/sharp-darwin-arm64@npm:0.34.5"
|
||||
dependencies:
|
||||
"@img/sharp-libvips-darwin-arm64": "npm:1.2.4"
|
||||
dependenciesMeta:
|
||||
"@img/sharp-libvips-darwin-arm64":
|
||||
optional: true
|
||||
conditions: os=darwin & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-darwin-x64@npm:0.34.5":
|
||||
version: 0.34.5
|
||||
resolution: "@img/sharp-darwin-x64@npm:0.34.5"
|
||||
dependencies:
|
||||
"@img/sharp-libvips-darwin-x64": "npm:1.2.4"
|
||||
dependenciesMeta:
|
||||
"@img/sharp-libvips-darwin-x64":
|
||||
optional: true
|
||||
conditions: os=darwin & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-libvips-darwin-arm64@npm:1.2.4":
|
||||
version: 1.2.4
|
||||
resolution: "@img/sharp-libvips-darwin-arm64@npm:1.2.4"
|
||||
conditions: os=darwin & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-libvips-darwin-x64@npm:1.2.4":
|
||||
version: 1.2.4
|
||||
resolution: "@img/sharp-libvips-darwin-x64@npm:1.2.4"
|
||||
conditions: os=darwin & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-libvips-linux-arm64@npm:1.2.4":
|
||||
version: 1.2.4
|
||||
resolution: "@img/sharp-libvips-linux-arm64@npm:1.2.4"
|
||||
conditions: os=linux & cpu=arm64 & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-libvips-linux-arm@npm:1.2.4":
|
||||
version: 1.2.4
|
||||
resolution: "@img/sharp-libvips-linux-arm@npm:1.2.4"
|
||||
conditions: os=linux & cpu=arm & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-libvips-linux-ppc64@npm:1.2.4":
|
||||
version: 1.2.4
|
||||
resolution: "@img/sharp-libvips-linux-ppc64@npm:1.2.4"
|
||||
conditions: os=linux & cpu=ppc64 & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-libvips-linux-riscv64@npm:1.2.4":
|
||||
version: 1.2.4
|
||||
resolution: "@img/sharp-libvips-linux-riscv64@npm:1.2.4"
|
||||
conditions: os=linux & cpu=riscv64 & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-libvips-linux-s390x@npm:1.2.4":
|
||||
version: 1.2.4
|
||||
resolution: "@img/sharp-libvips-linux-s390x@npm:1.2.4"
|
||||
conditions: os=linux & cpu=s390x & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-libvips-linux-x64@npm:1.2.4":
|
||||
version: 1.2.4
|
||||
resolution: "@img/sharp-libvips-linux-x64@npm:1.2.4"
|
||||
conditions: os=linux & cpu=x64 & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-libvips-linuxmusl-arm64@npm:1.2.4":
|
||||
version: 1.2.4
|
||||
resolution: "@img/sharp-libvips-linuxmusl-arm64@npm:1.2.4"
|
||||
conditions: os=linux & cpu=arm64 & libc=musl
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-libvips-linuxmusl-x64@npm:1.2.4":
|
||||
version: 1.2.4
|
||||
resolution: "@img/sharp-libvips-linuxmusl-x64@npm:1.2.4"
|
||||
conditions: os=linux & cpu=x64 & libc=musl
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-linux-arm64@npm:0.34.5":
|
||||
version: 0.34.5
|
||||
resolution: "@img/sharp-linux-arm64@npm:0.34.5"
|
||||
dependencies:
|
||||
"@img/sharp-libvips-linux-arm64": "npm:1.2.4"
|
||||
dependenciesMeta:
|
||||
"@img/sharp-libvips-linux-arm64":
|
||||
optional: true
|
||||
conditions: os=linux & cpu=arm64 & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-linux-arm@npm:0.34.5":
|
||||
version: 0.34.5
|
||||
resolution: "@img/sharp-linux-arm@npm:0.34.5"
|
||||
dependencies:
|
||||
"@img/sharp-libvips-linux-arm": "npm:1.2.4"
|
||||
dependenciesMeta:
|
||||
"@img/sharp-libvips-linux-arm":
|
||||
optional: true
|
||||
conditions: os=linux & cpu=arm & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-linux-ppc64@npm:0.34.5":
|
||||
version: 0.34.5
|
||||
resolution: "@img/sharp-linux-ppc64@npm:0.34.5"
|
||||
dependencies:
|
||||
"@img/sharp-libvips-linux-ppc64": "npm:1.2.4"
|
||||
dependenciesMeta:
|
||||
"@img/sharp-libvips-linux-ppc64":
|
||||
optional: true
|
||||
conditions: os=linux & cpu=ppc64 & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-linux-riscv64@npm:0.34.5":
|
||||
version: 0.34.5
|
||||
resolution: "@img/sharp-linux-riscv64@npm:0.34.5"
|
||||
dependencies:
|
||||
"@img/sharp-libvips-linux-riscv64": "npm:1.2.4"
|
||||
dependenciesMeta:
|
||||
"@img/sharp-libvips-linux-riscv64":
|
||||
optional: true
|
||||
conditions: os=linux & cpu=riscv64 & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-linux-s390x@npm:0.34.5":
|
||||
version: 0.34.5
|
||||
resolution: "@img/sharp-linux-s390x@npm:0.34.5"
|
||||
dependencies:
|
||||
"@img/sharp-libvips-linux-s390x": "npm:1.2.4"
|
||||
dependenciesMeta:
|
||||
"@img/sharp-libvips-linux-s390x":
|
||||
optional: true
|
||||
conditions: os=linux & cpu=s390x & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-linux-x64@npm:0.34.5":
|
||||
version: 0.34.5
|
||||
resolution: "@img/sharp-linux-x64@npm:0.34.5"
|
||||
dependencies:
|
||||
"@img/sharp-libvips-linux-x64": "npm:1.2.4"
|
||||
dependenciesMeta:
|
||||
"@img/sharp-libvips-linux-x64":
|
||||
optional: true
|
||||
conditions: os=linux & cpu=x64 & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-linuxmusl-arm64@npm:0.34.5":
|
||||
version: 0.34.5
|
||||
resolution: "@img/sharp-linuxmusl-arm64@npm:0.34.5"
|
||||
dependencies:
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "npm:1.2.4"
|
||||
dependenciesMeta:
|
||||
"@img/sharp-libvips-linuxmusl-arm64":
|
||||
optional: true
|
||||
conditions: os=linux & cpu=arm64 & libc=musl
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-linuxmusl-x64@npm:0.34.5":
|
||||
version: 0.34.5
|
||||
resolution: "@img/sharp-linuxmusl-x64@npm:0.34.5"
|
||||
dependencies:
|
||||
"@img/sharp-libvips-linuxmusl-x64": "npm:1.2.4"
|
||||
dependenciesMeta:
|
||||
"@img/sharp-libvips-linuxmusl-x64":
|
||||
optional: true
|
||||
conditions: os=linux & cpu=x64 & libc=musl
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-wasm32@npm:0.34.5":
|
||||
version: 0.34.5
|
||||
resolution: "@img/sharp-wasm32@npm:0.34.5"
|
||||
dependencies:
|
||||
"@emnapi/runtime": "npm:^1.7.0"
|
||||
conditions: cpu=wasm32
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-win32-arm64@npm:0.34.5":
|
||||
version: 0.34.5
|
||||
resolution: "@img/sharp-win32-arm64@npm:0.34.5"
|
||||
conditions: os=win32 & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-win32-ia32@npm:0.34.5":
|
||||
version: 0.34.5
|
||||
resolution: "@img/sharp-win32-ia32@npm:0.34.5"
|
||||
conditions: os=win32 & cpu=ia32
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-win32-x64@npm:0.34.5":
|
||||
version: 0.34.5
|
||||
resolution: "@img/sharp-win32-x64@npm:0.34.5"
|
||||
conditions: os=win32 & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@inquirer/ansi@npm:^2.0.7":
|
||||
version: 2.0.7
|
||||
resolution: "@inquirer/ansi@npm:2.0.7"
|
||||
@@ -1002,8 +1238,8 @@ __metadata:
|
||||
oxlint: "npm:^0.16.0"
|
||||
react: "npm:^19.0.0"
|
||||
react-dom: "npm:^19.0.0"
|
||||
twenty-client-sdk: "npm:2.15.0"
|
||||
twenty-sdk: "npm:2.15.0"
|
||||
twenty-client-sdk: "npm:2.16.0"
|
||||
twenty-sdk: "npm:2.16.0"
|
||||
typescript: "npm:^5.9.3"
|
||||
vite-tsconfig-paths: "npm:^4.2.1"
|
||||
vitest: "npm:^4.1.9"
|
||||
@@ -1539,7 +1775,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"detect-libc@npm:^2.0.3":
|
||||
"detect-libc@npm:^2.0.3, detect-libc@npm:^2.1.2":
|
||||
version: 2.1.2
|
||||
resolution: "detect-libc@npm:2.1.2"
|
||||
checksum: 10c0/acc675c29a5649fa1fb6e255f993b8ee829e510b6b56b0910666949c80c364738833417d0edb5f90e4e46be17228b0f2b66a010513984e18b15deeeac49369c4
|
||||
@@ -2775,6 +3011,99 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"semver@npm:^7.7.3":
|
||||
version: 7.8.5
|
||||
resolution: "semver@npm:7.8.5"
|
||||
bin:
|
||||
semver: bin/semver.js
|
||||
checksum: 10c0/b1f3127a5be8125a94f37188b361c212466c292c6910adce3ec106cff5dc211ccaedc4739c11bb70fda59d6fc1f040a9bca289f4e093451521a2372e5231fe0c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"sharp@npm:^0.34.5":
|
||||
version: 0.34.5
|
||||
resolution: "sharp@npm:0.34.5"
|
||||
dependencies:
|
||||
"@img/colour": "npm:^1.0.0"
|
||||
"@img/sharp-darwin-arm64": "npm:0.34.5"
|
||||
"@img/sharp-darwin-x64": "npm:0.34.5"
|
||||
"@img/sharp-libvips-darwin-arm64": "npm:1.2.4"
|
||||
"@img/sharp-libvips-darwin-x64": "npm:1.2.4"
|
||||
"@img/sharp-libvips-linux-arm": "npm:1.2.4"
|
||||
"@img/sharp-libvips-linux-arm64": "npm:1.2.4"
|
||||
"@img/sharp-libvips-linux-ppc64": "npm:1.2.4"
|
||||
"@img/sharp-libvips-linux-riscv64": "npm:1.2.4"
|
||||
"@img/sharp-libvips-linux-s390x": "npm:1.2.4"
|
||||
"@img/sharp-libvips-linux-x64": "npm:1.2.4"
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "npm:1.2.4"
|
||||
"@img/sharp-libvips-linuxmusl-x64": "npm:1.2.4"
|
||||
"@img/sharp-linux-arm": "npm:0.34.5"
|
||||
"@img/sharp-linux-arm64": "npm:0.34.5"
|
||||
"@img/sharp-linux-ppc64": "npm:0.34.5"
|
||||
"@img/sharp-linux-riscv64": "npm:0.34.5"
|
||||
"@img/sharp-linux-s390x": "npm:0.34.5"
|
||||
"@img/sharp-linux-x64": "npm:0.34.5"
|
||||
"@img/sharp-linuxmusl-arm64": "npm:0.34.5"
|
||||
"@img/sharp-linuxmusl-x64": "npm:0.34.5"
|
||||
"@img/sharp-wasm32": "npm:0.34.5"
|
||||
"@img/sharp-win32-arm64": "npm:0.34.5"
|
||||
"@img/sharp-win32-ia32": "npm:0.34.5"
|
||||
"@img/sharp-win32-x64": "npm:0.34.5"
|
||||
detect-libc: "npm:^2.1.2"
|
||||
semver: "npm:^7.7.3"
|
||||
dependenciesMeta:
|
||||
"@img/sharp-darwin-arm64":
|
||||
optional: true
|
||||
"@img/sharp-darwin-x64":
|
||||
optional: true
|
||||
"@img/sharp-libvips-darwin-arm64":
|
||||
optional: true
|
||||
"@img/sharp-libvips-darwin-x64":
|
||||
optional: true
|
||||
"@img/sharp-libvips-linux-arm":
|
||||
optional: true
|
||||
"@img/sharp-libvips-linux-arm64":
|
||||
optional: true
|
||||
"@img/sharp-libvips-linux-ppc64":
|
||||
optional: true
|
||||
"@img/sharp-libvips-linux-riscv64":
|
||||
optional: true
|
||||
"@img/sharp-libvips-linux-s390x":
|
||||
optional: true
|
||||
"@img/sharp-libvips-linux-x64":
|
||||
optional: true
|
||||
"@img/sharp-libvips-linuxmusl-arm64":
|
||||
optional: true
|
||||
"@img/sharp-libvips-linuxmusl-x64":
|
||||
optional: true
|
||||
"@img/sharp-linux-arm":
|
||||
optional: true
|
||||
"@img/sharp-linux-arm64":
|
||||
optional: true
|
||||
"@img/sharp-linux-ppc64":
|
||||
optional: true
|
||||
"@img/sharp-linux-riscv64":
|
||||
optional: true
|
||||
"@img/sharp-linux-s390x":
|
||||
optional: true
|
||||
"@img/sharp-linux-x64":
|
||||
optional: true
|
||||
"@img/sharp-linuxmusl-arm64":
|
||||
optional: true
|
||||
"@img/sharp-linuxmusl-x64":
|
||||
optional: true
|
||||
"@img/sharp-wasm32":
|
||||
optional: true
|
||||
"@img/sharp-win32-arm64":
|
||||
optional: true
|
||||
"@img/sharp-win32-ia32":
|
||||
optional: true
|
||||
"@img/sharp-win32-x64":
|
||||
optional: true
|
||||
checksum: 10c0/fd79e29df0597a7d5704b8461c51f944ead91a5243691697be6e8243b966402beda53ddc6f0a53b96ea3cb8221f0b244aa588114d3ebf8734fb4aefd41ab802f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"siginfo@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "siginfo@npm:2.0.0"
|
||||
@@ -3002,22 +3331,22 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"twenty-client-sdk@npm:2.15.0":
|
||||
version: 2.15.0
|
||||
resolution: "twenty-client-sdk@npm:2.15.0"
|
||||
"twenty-client-sdk@npm:2.16.0":
|
||||
version: 2.16.0
|
||||
resolution: "twenty-client-sdk@npm:2.16.0"
|
||||
dependencies:
|
||||
"@genql/runtime": "npm:^2.10.0"
|
||||
esbuild: "npm:^0.28.1"
|
||||
graphql: "npm:^16.8.1"
|
||||
lodash: "npm:^4.17.21"
|
||||
prettier: "npm:^3.8.3"
|
||||
checksum: 10c0/ea5143511ec3d42a0c2eaabda8833bac5c73c25fe04ba967d76b47b22c67468dd59d9e748a00e99a8085ffdb02d6cc5f41556f9e85a9785b2909b63adcb4205f
|
||||
checksum: 10c0/f990220bec103a04ca50dc8bb38c295e94a4e51b27a3188f4a0ae515c204b76bf87f5591739eb705b97e7aa5a9aa6a215d0d4da05297e50a0392801ea64a2234
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"twenty-sdk@npm:2.15.0":
|
||||
version: 2.15.0
|
||||
resolution: "twenty-sdk@npm:2.15.0"
|
||||
"twenty-sdk@npm:2.16.0":
|
||||
version: 2.16.0
|
||||
resolution: "twenty-sdk@npm:2.16.0"
|
||||
dependencies:
|
||||
"@sniptt/guards": "npm:^0.2.0"
|
||||
axios: "npm:^1.16.0"
|
||||
@@ -3034,13 +3363,14 @@ __metadata:
|
||||
react: "npm:^19.2.0"
|
||||
react-dom: "npm:^19.2.0"
|
||||
semver: "npm:7.6.3"
|
||||
sharp: "npm:^0.34.5"
|
||||
tinyglobby: "npm:^0.2.15"
|
||||
twenty-client-sdk: "npm:2.15.0"
|
||||
twenty-client-sdk: "npm:2.16.0"
|
||||
typescript: "npm:^5.9.3"
|
||||
uuid: "npm:^13.0.2"
|
||||
bin:
|
||||
twenty: dist/cli.cjs
|
||||
checksum: 10c0/0fe9a3653f3adaa54eb8398e520328ed2fd51b4c4284b4ed4105dfd92e3c866f2e9b5c6933f952e974684cd38169cdb26d64cfe06f058f4f4cb5bb1f6be7d2fc
|
||||
checksum: 10c0/528f275f8bda022061a52c3563af514d2257f291fc5a232be90ca6011c50ff18a250984b6c68fc8e4c690644c92e1ccb0b89c511b8a19d832455c8860f2751a8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user