@contractspec/integration.providers-impls 1.57.0 → 1.58.0
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.
- package/dist/analytics.d.ts +1 -8
- package/dist/analytics.d.ts.map +1 -1
- package/dist/analytics.js +3 -3
- package/dist/calendar.d.ts +1 -8
- package/dist/calendar.d.ts.map +1 -1
- package/dist/calendar.js +3 -3
- package/dist/database.d.ts +1 -8
- package/dist/database.d.ts.map +1 -1
- package/dist/database.js +3 -3
- package/dist/email.d.ts +1 -8
- package/dist/email.d.ts.map +1 -1
- package/dist/email.js +3 -3
- package/dist/embedding.d.ts +1 -8
- package/dist/embedding.d.ts.map +1 -1
- package/dist/embedding.js +3 -3
- package/dist/impls/elevenlabs-voice.d.ts +14 -18
- package/dist/impls/elevenlabs-voice.d.ts.map +1 -1
- package/dist/impls/elevenlabs-voice.js +98 -88
- package/dist/impls/fal-voice.d.ts +22 -26
- package/dist/impls/fal-voice.d.ts.map +1 -1
- package/dist/impls/fal-voice.js +103 -78
- package/dist/impls/fathom-meeting-recorder.d.ts +35 -39
- package/dist/impls/fathom-meeting-recorder.d.ts.map +1 -1
- package/dist/impls/fathom-meeting-recorder.js +285 -142
- package/dist/impls/fathom-meeting-recorder.mapper.d.ts +4 -8
- package/dist/impls/fathom-meeting-recorder.mapper.d.ts.map +1 -1
- package/dist/impls/fathom-meeting-recorder.mapper.js +102 -38
- package/dist/impls/fathom-meeting-recorder.types.d.ts +16 -20
- package/dist/impls/fathom-meeting-recorder.types.d.ts.map +1 -1
- package/dist/impls/fathom-meeting-recorder.types.js +1 -0
- package/dist/impls/fathom-meeting-recorder.utils.d.ts +10 -14
- package/dist/impls/fathom-meeting-recorder.utils.d.ts.map +1 -1
- package/dist/impls/fathom-meeting-recorder.utils.js +58 -41
- package/dist/impls/fathom-meeting-recorder.webhooks.d.ts +3 -7
- package/dist/impls/fathom-meeting-recorder.webhooks.d.ts.map +1 -1
- package/dist/impls/fathom-meeting-recorder.webhooks.js +25 -20
- package/dist/impls/fireflies-meeting-recorder.d.ts +21 -25
- package/dist/impls/fireflies-meeting-recorder.d.ts.map +1 -1
- package/dist/impls/fireflies-meeting-recorder.js +272 -149
- package/dist/impls/fireflies-meeting-recorder.queries.d.ts +3 -6
- package/dist/impls/fireflies-meeting-recorder.queries.d.ts.map +1 -1
- package/dist/impls/fireflies-meeting-recorder.queries.js +10 -8
- package/dist/impls/fireflies-meeting-recorder.types.d.ts +26 -29
- package/dist/impls/fireflies-meeting-recorder.types.d.ts.map +1 -1
- package/dist/impls/fireflies-meeting-recorder.types.js +1 -0
- package/dist/impls/fireflies-meeting-recorder.utils.d.ts +4 -7
- package/dist/impls/fireflies-meeting-recorder.utils.d.ts.map +1 -1
- package/dist/impls/fireflies-meeting-recorder.utils.js +34 -27
- package/dist/impls/gcs-storage.d.ts +18 -22
- package/dist/impls/gcs-storage.d.ts.map +1 -1
- package/dist/impls/gcs-storage.js +92 -84
- package/dist/impls/gmail-inbound.d.ts +20 -24
- package/dist/impls/gmail-inbound.d.ts.map +1 -1
- package/dist/impls/gmail-inbound.js +212 -185
- package/dist/impls/gmail-outbound.d.ts +12 -16
- package/dist/impls/gmail-outbound.d.ts.map +1 -1
- package/dist/impls/gmail-outbound.js +126 -92
- package/dist/impls/google-calendar.d.ts +17 -21
- package/dist/impls/google-calendar.d.ts.map +1 -1
- package/dist/impls/google-calendar.js +182 -145
- package/dist/impls/gradium-voice.d.ts +20 -22
- package/dist/impls/gradium-voice.d.ts.map +1 -1
- package/dist/impls/gradium-voice.js +85 -74
- package/dist/impls/granola-meeting-recorder.d.ts +31 -24
- package/dist/impls/granola-meeting-recorder.d.ts.map +1 -1
- package/dist/impls/granola-meeting-recorder.js +511 -143
- package/dist/impls/granola-meeting-recorder.mcp.d.ts +25 -0
- package/dist/impls/granola-meeting-recorder.mcp.d.ts.map +1 -0
- package/dist/impls/granola-meeting-recorder.mcp.js +279 -0
- package/dist/impls/granola-meeting-recorder.types.d.ts +60 -49
- package/dist/impls/granola-meeting-recorder.types.d.ts.map +1 -1
- package/dist/impls/granola-meeting-recorder.types.js +1 -0
- package/dist/impls/index.d.ts +28 -28
- package/dist/impls/index.d.ts.map +1 -0
- package/dist/impls/index.js +4659 -29
- package/dist/impls/jira.d.ts +18 -22
- package/dist/impls/jira.d.ts.map +1 -1
- package/dist/impls/jira.js +112 -101
- package/dist/impls/linear.d.ts +17 -21
- package/dist/impls/linear.d.ts.map +1 -1
- package/dist/impls/linear.js +78 -69
- package/dist/impls/mistral-embedding.d.ts +17 -21
- package/dist/impls/mistral-embedding.d.ts.map +1 -1
- package/dist/impls/mistral-embedding.js +41 -39
- package/dist/impls/mistral-llm.d.ts +25 -29
- package/dist/impls/mistral-llm.d.ts.map +1 -1
- package/dist/impls/mistral-llm.js +266 -244
- package/dist/impls/notion.d.ts +20 -24
- package/dist/impls/notion.d.ts.map +1 -1
- package/dist/impls/notion.js +145 -110
- package/dist/impls/posthog-reader.d.ts +18 -22
- package/dist/impls/posthog-reader.d.ts.map +1 -1
- package/dist/impls/posthog-reader.js +148 -129
- package/dist/impls/posthog-utils.d.ts +4 -7
- package/dist/impls/posthog-utils.d.ts.map +1 -1
- package/dist/impls/posthog-utils.js +31 -22
- package/dist/impls/posthog.d.ts +33 -37
- package/dist/impls/posthog.d.ts.map +1 -1
- package/dist/impls/posthog.js +320 -119
- package/dist/impls/postmark-email.d.ts +13 -17
- package/dist/impls/postmark-email.d.ts.map +1 -1
- package/dist/impls/postmark-email.js +55 -50
- package/dist/impls/powens-client.d.ts +111 -114
- package/dist/impls/powens-client.d.ts.map +1 -1
- package/dist/impls/powens-client.js +194 -170
- package/dist/impls/powens-openbanking.d.ts +22 -26
- package/dist/impls/powens-openbanking.d.ts.map +1 -1
- package/dist/impls/powens-openbanking.js +425 -217
- package/dist/impls/provider-factory.d.ts +29 -33
- package/dist/impls/provider-factory.d.ts.map +1 -1
- package/dist/impls/provider-factory.js +4072 -275
- package/dist/impls/qdrant-vector.d.ts +18 -22
- package/dist/impls/qdrant-vector.d.ts.map +1 -1
- package/dist/impls/qdrant-vector.js +76 -69
- package/dist/impls/stripe-payments.d.ts +22 -26
- package/dist/impls/stripe-payments.d.ts.map +1 -1
- package/dist/impls/stripe-payments.js +219 -193
- package/dist/impls/supabase-psql.d.ts +21 -25
- package/dist/impls/supabase-psql.d.ts.map +1 -1
- package/dist/impls/supabase-psql.js +138 -98
- package/dist/impls/supabase-vector.d.ts +29 -33
- package/dist/impls/supabase-vector.d.ts.map +1 -1
- package/dist/impls/supabase-vector.js +278 -103
- package/dist/impls/tldv-meeting-recorder.d.ts +18 -22
- package/dist/impls/tldv-meeting-recorder.d.ts.map +1 -1
- package/dist/impls/tldv-meeting-recorder.js +142 -127
- package/dist/impls/twilio-sms.d.ts +14 -17
- package/dist/impls/twilio-sms.d.ts.map +1 -1
- package/dist/impls/twilio-sms.js +62 -55
- package/dist/index.d.ts +15 -64
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4700 -107
- package/dist/llm.d.ts +1 -8
- package/dist/llm.d.ts.map +1 -1
- package/dist/llm.js +3 -3
- package/dist/meeting-recorder.d.ts +1 -8
- package/dist/meeting-recorder.d.ts.map +1 -1
- package/dist/meeting-recorder.js +3 -3
- package/dist/node/analytics.js +2 -0
- package/dist/node/calendar.js +2 -0
- package/dist/node/database.js +2 -0
- package/dist/node/email.js +2 -0
- package/dist/node/embedding.js +2 -0
- package/dist/node/impls/elevenlabs-voice.js +102 -0
- package/dist/node/impls/fal-voice.js +112 -0
- package/dist/node/impls/fathom-meeting-recorder.js +287 -0
- package/dist/node/impls/fathom-meeting-recorder.mapper.js +105 -0
- package/dist/node/impls/fathom-meeting-recorder.types.js +0 -0
- package/dist/node/impls/fathom-meeting-recorder.utils.js +72 -0
- package/dist/node/impls/fathom-meeting-recorder.webhooks.js +29 -0
- package/dist/node/impls/fireflies-meeting-recorder.js +274 -0
- package/dist/node/impls/fireflies-meeting-recorder.queries.js +85 -0
- package/dist/node/impls/fireflies-meeting-recorder.types.js +0 -0
- package/dist/node/impls/fireflies-meeting-recorder.utils.js +42 -0
- package/dist/node/impls/gcs-storage.js +97 -0
- package/dist/node/impls/gmail-inbound.js +227 -0
- package/dist/node/impls/gmail-outbound.js +139 -0
- package/dist/node/impls/google-calendar.js +191 -0
- package/dist/node/impls/gradium-voice.js +90 -0
- package/dist/node/impls/granola-meeting-recorder.js +512 -0
- package/dist/node/impls/granola-meeting-recorder.mcp.js +278 -0
- package/dist/node/impls/granola-meeting-recorder.types.js +0 -0
- package/dist/node/impls/index.js +4658 -0
- package/dist/node/impls/jira.js +124 -0
- package/dist/node/impls/linear.js +83 -0
- package/dist/node/impls/mistral-embedding.js +43 -0
- package/dist/node/impls/mistral-llm.js +269 -0
- package/dist/node/impls/notion.js +160 -0
- package/dist/node/impls/posthog-reader.js +159 -0
- package/dist/node/impls/posthog-utils.js +38 -0
- package/dist/node/impls/posthog.js +322 -0
- package/dist/node/impls/postmark-email.js +60 -0
- package/dist/node/impls/powens-client.js +195 -0
- package/dist/node/impls/powens-openbanking.js +426 -0
- package/dist/node/impls/provider-factory.js +4080 -0
- package/dist/node/impls/qdrant-vector.js +78 -0
- package/dist/node/impls/stripe-payments.js +228 -0
- package/dist/node/impls/supabase-psql.js +150 -0
- package/dist/node/impls/supabase-vector.js +323 -0
- package/dist/node/impls/tldv-meeting-recorder.js +145 -0
- package/dist/node/impls/twilio-sms.js +65 -0
- package/dist/node/index.js +4699 -0
- package/dist/node/llm.js +2 -0
- package/dist/node/meeting-recorder.js +2 -0
- package/dist/node/openbanking.js +2 -0
- package/dist/node/payments.js +2 -0
- package/dist/node/project-management.js +2 -0
- package/dist/node/runtime.js +0 -0
- package/dist/node/secrets/provider.js +11 -0
- package/dist/node/sms.js +2 -0
- package/dist/node/storage.js +2 -0
- package/dist/node/vector-store.js +2 -0
- package/dist/node/voice.js +2 -0
- package/dist/openbanking.d.ts +1 -8
- package/dist/openbanking.d.ts.map +1 -1
- package/dist/openbanking.js +3 -3
- package/dist/payments.d.ts +1 -8
- package/dist/payments.d.ts.map +1 -1
- package/dist/payments.js +3 -3
- package/dist/project-management.d.ts +1 -8
- package/dist/project-management.d.ts.map +1 -1
- package/dist/project-management.js +3 -3
- package/dist/runtime.d.ts +2 -2
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +1 -0
- package/dist/secrets/provider.d.ts +3 -2
- package/dist/secrets/provider.d.ts.map +1 -0
- package/dist/secrets/provider.js +12 -3
- package/dist/sms.d.ts +1 -8
- package/dist/sms.d.ts.map +1 -1
- package/dist/sms.js +3 -3
- package/dist/storage.d.ts +1 -8
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +3 -3
- package/dist/vector-store.d.ts +1 -8
- package/dist/vector-store.d.ts.map +1 -1
- package/dist/vector-store.js +3 -3
- package/dist/voice.d.ts +1 -8
- package/dist/voice.d.ts.map +1 -1
- package/dist/voice.js +3 -3
- package/package.json +405 -114
- package/dist/_virtual/_rolldown/runtime.js +0 -36
- package/dist/impls/elevenlabs-voice.js.map +0 -1
- package/dist/impls/fal-voice.js.map +0 -1
- package/dist/impls/fathom-meeting-recorder.js.map +0 -1
- package/dist/impls/fathom-meeting-recorder.mapper.js.map +0 -1
- package/dist/impls/fathom-meeting-recorder.utils.js.map +0 -1
- package/dist/impls/fathom-meeting-recorder.webhooks.js.map +0 -1
- package/dist/impls/fireflies-meeting-recorder.js.map +0 -1
- package/dist/impls/fireflies-meeting-recorder.queries.js.map +0 -1
- package/dist/impls/fireflies-meeting-recorder.utils.js.map +0 -1
- package/dist/impls/gcs-storage.js.map +0 -1
- package/dist/impls/gmail-inbound.js.map +0 -1
- package/dist/impls/gmail-outbound.js.map +0 -1
- package/dist/impls/google-calendar.js.map +0 -1
- package/dist/impls/gradium-voice.js.map +0 -1
- package/dist/impls/granola-meeting-recorder.js.map +0 -1
- package/dist/impls/jira.js.map +0 -1
- package/dist/impls/linear.js.map +0 -1
- package/dist/impls/mistral-embedding.js.map +0 -1
- package/dist/impls/mistral-llm.js.map +0 -1
- package/dist/impls/notion.js.map +0 -1
- package/dist/impls/posthog-reader.js.map +0 -1
- package/dist/impls/posthog-utils.js.map +0 -1
- package/dist/impls/posthog.js.map +0 -1
- package/dist/impls/postmark-email.js.map +0 -1
- package/dist/impls/powens-client.js.map +0 -1
- package/dist/impls/powens-openbanking.js.map +0 -1
- package/dist/impls/provider-factory.js.map +0 -1
- package/dist/impls/qdrant-vector.js.map +0 -1
- package/dist/impls/stripe-payments.js.map +0 -1
- package/dist/impls/supabase-psql.js.map +0 -1
- package/dist/impls/supabase-vector.js.map +0 -1
- package/dist/impls/tldv-meeting-recorder.js.map +0 -1
- package/dist/impls/twilio-sms.js.map +0 -1
- package/dist/index.js.map +0 -1
|
@@ -0,0 +1,4699 @@
|
|
|
1
|
+
// src/analytics.ts
|
|
2
|
+
export * from "@contractspec/lib.contracts/integrations/providers/analytics";
|
|
3
|
+
|
|
4
|
+
// src/calendar.ts
|
|
5
|
+
export * from "@contractspec/lib.contracts/integrations/providers/calendar";
|
|
6
|
+
|
|
7
|
+
// src/database.ts
|
|
8
|
+
export * from "@contractspec/lib.contracts/integrations/providers/database";
|
|
9
|
+
|
|
10
|
+
// src/email.ts
|
|
11
|
+
export * from "@contractspec/lib.contracts/integrations/providers/email";
|
|
12
|
+
|
|
13
|
+
// src/embedding.ts
|
|
14
|
+
export * from "@contractspec/lib.contracts/integrations/providers/embedding";
|
|
15
|
+
|
|
16
|
+
// src/impls/elevenlabs-voice.ts
|
|
17
|
+
import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js";
|
|
18
|
+
var FORMAT_MAP = {
|
|
19
|
+
mp3: "mp3_44100_128",
|
|
20
|
+
wav: "pcm_44100",
|
|
21
|
+
ogg: "mp3_44100_128",
|
|
22
|
+
pcm: "pcm_16000"
|
|
23
|
+
};
|
|
24
|
+
var SAMPLE_RATE = {
|
|
25
|
+
mp3_22050_32: 22050,
|
|
26
|
+
mp3_44100_32: 44100,
|
|
27
|
+
mp3_44100_64: 44100,
|
|
28
|
+
mp3_44100_96: 44100,
|
|
29
|
+
mp3_44100_128: 44100,
|
|
30
|
+
mp3_44100_192: 44100,
|
|
31
|
+
pcm_16000: 16000,
|
|
32
|
+
pcm_22050: 22050,
|
|
33
|
+
pcm_24000: 24000,
|
|
34
|
+
pcm_44100: 44100,
|
|
35
|
+
ulaw_8000: 8000
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
class ElevenLabsVoiceProvider {
|
|
39
|
+
client;
|
|
40
|
+
defaultVoiceId;
|
|
41
|
+
modelId;
|
|
42
|
+
constructor(options) {
|
|
43
|
+
this.client = options.client ?? new ElevenLabsClient({
|
|
44
|
+
apiKey: options.apiKey
|
|
45
|
+
});
|
|
46
|
+
this.defaultVoiceId = options.defaultVoiceId;
|
|
47
|
+
this.modelId = options.modelId;
|
|
48
|
+
}
|
|
49
|
+
async listVoices() {
|
|
50
|
+
const response = await this.client.voices.getAll();
|
|
51
|
+
return (response.voices ?? []).map((voice) => ({
|
|
52
|
+
id: voice.voiceId ?? "",
|
|
53
|
+
name: voice.name ?? "",
|
|
54
|
+
description: voice.description ?? undefined,
|
|
55
|
+
language: voice.labels?.language ?? undefined,
|
|
56
|
+
metadata: {
|
|
57
|
+
category: voice.category ?? "",
|
|
58
|
+
...voice.previewUrl ? { previewUrl: voice.previewUrl } : {},
|
|
59
|
+
...(() => {
|
|
60
|
+
const { language, ...rest } = voice.labels ?? {};
|
|
61
|
+
return rest;
|
|
62
|
+
})()
|
|
63
|
+
}
|
|
64
|
+
}));
|
|
65
|
+
}
|
|
66
|
+
async synthesize(input) {
|
|
67
|
+
const voiceId = input.voiceId ?? this.defaultVoiceId;
|
|
68
|
+
if (!voiceId) {
|
|
69
|
+
throw new Error("Voice ID is required for ElevenLabs synthesis.");
|
|
70
|
+
}
|
|
71
|
+
const formatKey = input.format ?? "mp3";
|
|
72
|
+
const outputFormat = FORMAT_MAP[formatKey] ?? FORMAT_MAP.mp3;
|
|
73
|
+
const sampleRate = input.sampleRateHz ?? SAMPLE_RATE[outputFormat] ?? SAMPLE_RATE.mp3_44100_128 ?? 44100;
|
|
74
|
+
const voiceSettings = input.stability != null || input.similarityBoost != null || input.style != null ? {
|
|
75
|
+
...input.stability != null ? { stability: input.stability } : {},
|
|
76
|
+
...input.similarityBoost != null ? { similarityBoost: input.similarityBoost } : {},
|
|
77
|
+
...input.style != null ? { style: input.style } : {}
|
|
78
|
+
} : undefined;
|
|
79
|
+
const stream = await this.client.textToSpeech.convert(voiceId, {
|
|
80
|
+
text: input.text,
|
|
81
|
+
modelId: this.modelId,
|
|
82
|
+
outputFormat,
|
|
83
|
+
voiceSettings
|
|
84
|
+
});
|
|
85
|
+
const audio = await readWebStream(stream);
|
|
86
|
+
return {
|
|
87
|
+
audio,
|
|
88
|
+
format: formatKey,
|
|
89
|
+
sampleRateHz: sampleRate,
|
|
90
|
+
durationSeconds: undefined,
|
|
91
|
+
url: undefined
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async function readWebStream(stream) {
|
|
96
|
+
const reader = stream.getReader();
|
|
97
|
+
const chunks = [];
|
|
98
|
+
while (true) {
|
|
99
|
+
const { done, value } = await reader.read();
|
|
100
|
+
if (done)
|
|
101
|
+
break;
|
|
102
|
+
if (value) {
|
|
103
|
+
chunks.push(value);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const length = chunks.reduce((total, chunk) => total + chunk.length, 0);
|
|
107
|
+
const result = new Uint8Array(length);
|
|
108
|
+
let offset = 0;
|
|
109
|
+
for (const chunk of chunks) {
|
|
110
|
+
result.set(chunk, offset);
|
|
111
|
+
offset += chunk.length;
|
|
112
|
+
}
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// src/impls/fal-voice.ts
|
|
117
|
+
import { createFalClient } from "@fal-ai/client";
|
|
118
|
+
var DEFAULT_MODEL_ID = "fal-ai/chatterbox/text-to-speech";
|
|
119
|
+
|
|
120
|
+
class FalVoiceProvider {
|
|
121
|
+
client;
|
|
122
|
+
modelId;
|
|
123
|
+
defaultVoiceUrl;
|
|
124
|
+
defaultExaggeration;
|
|
125
|
+
defaultTemperature;
|
|
126
|
+
defaultCfg;
|
|
127
|
+
pollIntervalMs;
|
|
128
|
+
constructor(options) {
|
|
129
|
+
this.client = options.client ?? createFalClient({
|
|
130
|
+
credentials: options.apiKey
|
|
131
|
+
});
|
|
132
|
+
this.modelId = options.modelId ?? DEFAULT_MODEL_ID;
|
|
133
|
+
this.defaultVoiceUrl = options.defaultVoiceUrl;
|
|
134
|
+
this.defaultExaggeration = options.defaultExaggeration;
|
|
135
|
+
this.defaultTemperature = options.defaultTemperature;
|
|
136
|
+
this.defaultCfg = options.defaultCfg;
|
|
137
|
+
this.pollIntervalMs = options.pollIntervalMs;
|
|
138
|
+
}
|
|
139
|
+
async listVoices() {
|
|
140
|
+
const voices = [
|
|
141
|
+
{
|
|
142
|
+
id: "default",
|
|
143
|
+
name: "Default Chatterbox Voice",
|
|
144
|
+
description: "Uses the default model voice (or configured default reference audio URL).",
|
|
145
|
+
metadata: {
|
|
146
|
+
modelId: this.modelId
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
];
|
|
150
|
+
if (this.defaultVoiceUrl) {
|
|
151
|
+
voices.push({
|
|
152
|
+
id: this.defaultVoiceUrl,
|
|
153
|
+
name: "Configured Reference Voice",
|
|
154
|
+
description: "Reference voice configured at provider setup and used when voiceId is default.",
|
|
155
|
+
previewUrl: this.defaultVoiceUrl,
|
|
156
|
+
metadata: {
|
|
157
|
+
modelId: this.modelId,
|
|
158
|
+
source: "config.defaultVoiceUrl"
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
return voices;
|
|
163
|
+
}
|
|
164
|
+
async synthesize(input) {
|
|
165
|
+
const referenceVoiceUrl = resolveVoiceUrl(input.voiceId, this.defaultVoiceUrl);
|
|
166
|
+
const result = await this.client.subscribe(this.modelId, {
|
|
167
|
+
input: {
|
|
168
|
+
text: input.text,
|
|
169
|
+
...referenceVoiceUrl ? { audio_url: referenceVoiceUrl } : {},
|
|
170
|
+
...this.defaultExaggeration != null ? { exaggeration: this.defaultExaggeration } : {},
|
|
171
|
+
...this.defaultTemperature != null ? { temperature: this.defaultTemperature } : {},
|
|
172
|
+
...this.defaultCfg != null ? { cfg: this.defaultCfg } : {}
|
|
173
|
+
},
|
|
174
|
+
pollInterval: this.pollIntervalMs
|
|
175
|
+
});
|
|
176
|
+
const audioUrl = extractAudioUrl(result.data);
|
|
177
|
+
if (!audioUrl) {
|
|
178
|
+
throw new Error("Fal synthesis completed without an audio URL in response.");
|
|
179
|
+
}
|
|
180
|
+
const response = await fetch(audioUrl);
|
|
181
|
+
if (!response.ok) {
|
|
182
|
+
throw new Error(`Fal audio download failed (${response.status}).`);
|
|
183
|
+
}
|
|
184
|
+
const audio = new Uint8Array(await response.arrayBuffer());
|
|
185
|
+
return {
|
|
186
|
+
audio,
|
|
187
|
+
format: input.format ?? inferFormatFromUrl(audioUrl) ?? "wav",
|
|
188
|
+
sampleRateHz: input.sampleRateHz ?? 24000,
|
|
189
|
+
durationSeconds: undefined,
|
|
190
|
+
url: audioUrl
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function resolveVoiceUrl(voiceId, defaultVoiceUrl) {
|
|
195
|
+
if (!voiceId || voiceId === "default")
|
|
196
|
+
return defaultVoiceUrl;
|
|
197
|
+
if (isHttpUrl(voiceId))
|
|
198
|
+
return voiceId;
|
|
199
|
+
throw new Error('Fal voiceId must be "default" or a public reference audio URL.');
|
|
200
|
+
}
|
|
201
|
+
function extractAudioUrl(output) {
|
|
202
|
+
if (output.audio?.url)
|
|
203
|
+
return output.audio.url;
|
|
204
|
+
if (typeof output.audio_url === "string")
|
|
205
|
+
return output.audio_url;
|
|
206
|
+
if (typeof output.url === "string")
|
|
207
|
+
return output.url;
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
function inferFormatFromUrl(url) {
|
|
211
|
+
const normalized = url.toLowerCase();
|
|
212
|
+
if (normalized.endsWith(".wav"))
|
|
213
|
+
return "wav";
|
|
214
|
+
if (normalized.endsWith(".mp3"))
|
|
215
|
+
return "mp3";
|
|
216
|
+
if (normalized.endsWith(".ogg") || normalized.endsWith(".opus"))
|
|
217
|
+
return "ogg";
|
|
218
|
+
if (normalized.endsWith(".pcm"))
|
|
219
|
+
return "pcm";
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
function isHttpUrl(value) {
|
|
223
|
+
return value.startsWith("https://") || value.startsWith("http://");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// src/impls/fathom-meeting-recorder.utils.ts
|
|
227
|
+
function extractItems(page) {
|
|
228
|
+
if (Array.isArray(page.items))
|
|
229
|
+
return page.items;
|
|
230
|
+
if (Array.isArray(page.data)) {
|
|
231
|
+
return page.data;
|
|
232
|
+
}
|
|
233
|
+
return [];
|
|
234
|
+
}
|
|
235
|
+
function extractNextCursor(page) {
|
|
236
|
+
return page.nextCursor ?? page.next_cursor ?? undefined;
|
|
237
|
+
}
|
|
238
|
+
function mapInvitee(invitee) {
|
|
239
|
+
const email2 = invitee.email;
|
|
240
|
+
const name = invitee.name;
|
|
241
|
+
if (!email2 && !name)
|
|
242
|
+
return;
|
|
243
|
+
return {
|
|
244
|
+
email: email2,
|
|
245
|
+
name,
|
|
246
|
+
role: "attendee",
|
|
247
|
+
isExternal: invitee.is_external
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function matchRecordingId(meeting, targetId) {
|
|
251
|
+
return meeting.recordingId === targetId;
|
|
252
|
+
}
|
|
253
|
+
function durationSeconds(start, end) {
|
|
254
|
+
if (!start || !end)
|
|
255
|
+
return;
|
|
256
|
+
const startDate = start instanceof Date ? start : new Date(start);
|
|
257
|
+
const endDate = end instanceof Date ? end : new Date(end);
|
|
258
|
+
if (Number.isNaN(startDate.valueOf()) || Number.isNaN(endDate.valueOf())) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
return Math.max(0, (endDate.valueOf() - startDate.valueOf()) / 1000);
|
|
262
|
+
}
|
|
263
|
+
function mapTranscriptSegment(segment, index) {
|
|
264
|
+
return {
|
|
265
|
+
index,
|
|
266
|
+
speakerName: segment.speaker?.display_name ?? undefined,
|
|
267
|
+
speakerEmail: segment.speaker?.matched_calendar_invitee_email ?? undefined,
|
|
268
|
+
text: segment.text,
|
|
269
|
+
startTimeMs: parseTimestamp(segment.timestamp)
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
function parseTimestamp(value) {
|
|
273
|
+
const parts = value.split(":").map((part) => Number(part));
|
|
274
|
+
if (parts.length !== 3 || parts.some((part) => Number.isNaN(part))) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const [hours = 0, minutes = 0, seconds = 0] = parts;
|
|
278
|
+
return (hours * 3600 + minutes * 60 + seconds) * 1000;
|
|
279
|
+
}
|
|
280
|
+
async function safeReadError(response) {
|
|
281
|
+
try {
|
|
282
|
+
const data = await response.json();
|
|
283
|
+
return data?.message ?? response.statusText;
|
|
284
|
+
} catch {
|
|
285
|
+
return response.statusText;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// src/impls/fathom-meeting-recorder.mapper.ts
|
|
290
|
+
var mapFathomMeetingInvites = (invitees) => {
|
|
291
|
+
return invitees.map((invitee) => ({
|
|
292
|
+
...invitee,
|
|
293
|
+
name: invitee.name ?? undefined,
|
|
294
|
+
email: invitee.email ?? undefined
|
|
295
|
+
}));
|
|
296
|
+
};
|
|
297
|
+
function mapFathomMeeting(meeting, params) {
|
|
298
|
+
const connectionId = params.connectionId ?? "unknown";
|
|
299
|
+
return {
|
|
300
|
+
id: meeting.recordingId.toString(),
|
|
301
|
+
tenantId: params.tenantId,
|
|
302
|
+
connectionId,
|
|
303
|
+
externalId: meeting.recordingId.toString(),
|
|
304
|
+
title: meeting.title ?? meeting.meetingTitle,
|
|
305
|
+
organizer: meeting.recordedBy ? {
|
|
306
|
+
name: meeting.recordedBy.name ?? undefined,
|
|
307
|
+
email: meeting.recordedBy.email ?? undefined,
|
|
308
|
+
role: "organizer"
|
|
309
|
+
} : undefined,
|
|
310
|
+
invitees: meeting.calendarInvitees.length ? mapFathomMeetingInvites(meeting.calendarInvitees) : undefined,
|
|
311
|
+
participants: meeting.calendarInvitees.length ? mapFathomMeetingInvites(meeting.calendarInvitees) : undefined,
|
|
312
|
+
scheduledStartAt: meeting.scheduledStartTime?.toISOString(),
|
|
313
|
+
scheduledEndAt: meeting.scheduledEndTime?.toISOString(),
|
|
314
|
+
recordingStartAt: meeting.recordingStartTime?.toISOString(),
|
|
315
|
+
recordingEndAt: meeting.recordingEndTime?.toISOString(),
|
|
316
|
+
durationSeconds: durationSeconds(meeting.recordingStartTime, meeting.recordingEndTime),
|
|
317
|
+
meetingUrl: meeting.url ?? undefined,
|
|
318
|
+
shareUrl: meeting.shareUrl ?? undefined,
|
|
319
|
+
transcriptAvailable: Array.isArray(meeting.transcript),
|
|
320
|
+
sourcePlatform: "fathom",
|
|
321
|
+
language: meeting.transcriptLanguage,
|
|
322
|
+
metadata: {
|
|
323
|
+
calendarInviteesDomainsType: meeting.calendarInviteesDomainsType
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// src/impls/fathom-meeting-recorder.webhooks.ts
|
|
329
|
+
import { TriggeredFor } from "fathom-typescript/sdk/models/operations";
|
|
330
|
+
function normalizeWebhookHeaders(headers) {
|
|
331
|
+
const normalized = {};
|
|
332
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
333
|
+
if (value == null)
|
|
334
|
+
continue;
|
|
335
|
+
const normalizedKey = key.toLowerCase();
|
|
336
|
+
if (Array.isArray(value)) {
|
|
337
|
+
if (value.length === 0)
|
|
338
|
+
continue;
|
|
339
|
+
normalized[normalizedKey] = value.join(", ");
|
|
340
|
+
} else {
|
|
341
|
+
normalized[normalizedKey] = value;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return normalized;
|
|
345
|
+
}
|
|
346
|
+
function normalizeTriggeredFor(values) {
|
|
347
|
+
if (!values)
|
|
348
|
+
return;
|
|
349
|
+
const allowed = new Set(Object.values(TriggeredFor));
|
|
350
|
+
const normalized = values.map((value) => value.trim()).filter((value) => allowed.has(value));
|
|
351
|
+
return normalized.length ? normalized : undefined;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// src/impls/fathom-meeting-recorder.ts
|
|
355
|
+
import { Fathom } from "fathom-typescript";
|
|
356
|
+
import { TriggeredFor as TriggeredFor2 } from "fathom-typescript/sdk/models/operations";
|
|
357
|
+
var DEFAULT_BASE_URL = "https://api.fathom.ai/external/v1";
|
|
358
|
+
|
|
359
|
+
class FathomMeetingRecorderProvider {
|
|
360
|
+
client;
|
|
361
|
+
apiKey;
|
|
362
|
+
baseUrl;
|
|
363
|
+
includeTranscript;
|
|
364
|
+
includeSummary;
|
|
365
|
+
includeActionItems;
|
|
366
|
+
includeCrmMatches;
|
|
367
|
+
triggeredFor;
|
|
368
|
+
webhookSecret;
|
|
369
|
+
maxPages;
|
|
370
|
+
constructor(options) {
|
|
371
|
+
this.apiKey = options.apiKey;
|
|
372
|
+
this.baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
|
|
373
|
+
this.includeTranscript = options.includeTranscript ?? false;
|
|
374
|
+
this.includeSummary = options.includeSummary ?? false;
|
|
375
|
+
this.includeActionItems = options.includeActionItems ?? false;
|
|
376
|
+
this.includeCrmMatches = options.includeCrmMatches ?? false;
|
|
377
|
+
this.triggeredFor = options.triggeredFor;
|
|
378
|
+
this.webhookSecret = options.webhookSecret;
|
|
379
|
+
this.maxPages = options.maxPages ?? 5;
|
|
380
|
+
this.client = options.client ?? new Fathom({
|
|
381
|
+
serverURL: this.baseUrl,
|
|
382
|
+
security: {
|
|
383
|
+
apiKeyAuth: this.apiKey
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
async listMeetings(params) {
|
|
388
|
+
const request = {
|
|
389
|
+
cursor: params.cursor,
|
|
390
|
+
createdAfter: params.from,
|
|
391
|
+
createdBefore: params.to,
|
|
392
|
+
includeTranscript: params.includeTranscript ?? this.includeTranscript,
|
|
393
|
+
includeSummary: params.includeSummary ?? this.includeSummary,
|
|
394
|
+
includeActionItems: this.includeActionItems,
|
|
395
|
+
includeCrmMatches: this.includeCrmMatches
|
|
396
|
+
};
|
|
397
|
+
const result = await this.client.listMeetings(request);
|
|
398
|
+
let firstPage;
|
|
399
|
+
for await (const page of result) {
|
|
400
|
+
firstPage = page;
|
|
401
|
+
break;
|
|
402
|
+
}
|
|
403
|
+
if (!firstPage) {
|
|
404
|
+
return { meetings: [] };
|
|
405
|
+
}
|
|
406
|
+
const rawItems = extractItems(firstPage);
|
|
407
|
+
const meetings = rawItems.map((meeting) => mapFathomMeeting(meeting, params));
|
|
408
|
+
return {
|
|
409
|
+
meetings,
|
|
410
|
+
nextCursor: extractNextCursor(firstPage) ?? undefined,
|
|
411
|
+
hasMore: Boolean(extractNextCursor(firstPage))
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
async getMeeting(params) {
|
|
415
|
+
const result = await this.client.listMeetings({
|
|
416
|
+
includeTranscript: params.includeTranscript ?? this.includeTranscript,
|
|
417
|
+
includeSummary: params.includeSummary ?? this.includeSummary,
|
|
418
|
+
includeActionItems: this.includeActionItems,
|
|
419
|
+
includeCrmMatches: this.includeCrmMatches
|
|
420
|
+
});
|
|
421
|
+
let pageCount = 0;
|
|
422
|
+
const targetId = Number.parseInt(params.meetingId, 10);
|
|
423
|
+
for await (const page of result) {
|
|
424
|
+
pageCount += 1;
|
|
425
|
+
const match = extractItems(page).find((meeting) => matchRecordingId(meeting, targetId));
|
|
426
|
+
if (match) {
|
|
427
|
+
return mapFathomMeeting(match, params);
|
|
428
|
+
}
|
|
429
|
+
if (pageCount >= this.maxPages) {
|
|
430
|
+
break;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
throw new Error(`Fathom meeting "${targetId}" not found.`);
|
|
434
|
+
}
|
|
435
|
+
async getTranscript(params) {
|
|
436
|
+
const response = await this.request(`/recordings/${encodeURIComponent(params.meetingId)}/transcript`);
|
|
437
|
+
if (!Array.isArray(response.transcript)) {
|
|
438
|
+
throw new Error("Fathom transcript response did not include transcript.");
|
|
439
|
+
}
|
|
440
|
+
const segments = response.transcript.map((segment, index) => mapTranscriptSegment(segment, index));
|
|
441
|
+
return {
|
|
442
|
+
id: params.meetingId,
|
|
443
|
+
meetingId: params.meetingId,
|
|
444
|
+
tenantId: params.tenantId,
|
|
445
|
+
connectionId: params.connectionId ?? "unknown",
|
|
446
|
+
externalId: params.meetingId,
|
|
447
|
+
format: "segments",
|
|
448
|
+
text: segments.map((segment) => segment.text).join(`
|
|
449
|
+
`),
|
|
450
|
+
segments,
|
|
451
|
+
metadata: {
|
|
452
|
+
provider: "fathom"
|
|
453
|
+
},
|
|
454
|
+
raw: response.transcript
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
async parseWebhook(request) {
|
|
458
|
+
const payload = request.parsedBody ?? JSON.parse(request.rawBody);
|
|
459
|
+
const body = payload;
|
|
460
|
+
const recordingId = body.recording_id ?? body.recordingId ?? body.meeting_id ?? body.meetingId;
|
|
461
|
+
const verified = this.webhookSecret ? await this.verifyWebhook(request) : undefined;
|
|
462
|
+
return {
|
|
463
|
+
providerKey: "meeting-recorder.fathom",
|
|
464
|
+
eventType: body.event_type ?? body.eventType,
|
|
465
|
+
meetingId: recordingId,
|
|
466
|
+
recordingId,
|
|
467
|
+
verified,
|
|
468
|
+
payload
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
async verifyWebhook(request) {
|
|
472
|
+
if (!this.webhookSecret)
|
|
473
|
+
return true;
|
|
474
|
+
const headers = normalizeWebhookHeaders(request.headers);
|
|
475
|
+
try {
|
|
476
|
+
return Boolean(Fathom.verifyWebhook(this.webhookSecret, headers, request.rawBody));
|
|
477
|
+
} catch {
|
|
478
|
+
return false;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
async registerWebhook(registration) {
|
|
482
|
+
const triggeredFor = normalizeTriggeredFor(registration.triggeredFor) ?? normalizeTriggeredFor(this.triggeredFor) ?? [TriggeredFor2.MyRecordings];
|
|
483
|
+
const webhook = await this.client.createWebhook({
|
|
484
|
+
destinationUrl: registration.url,
|
|
485
|
+
includeTranscript: registration.includeTranscript ?? true,
|
|
486
|
+
includeSummary: registration.includeSummary ?? false,
|
|
487
|
+
includeActionItems: registration.includeActionItems ?? false,
|
|
488
|
+
includeCrmMatches: registration.includeCrmMatches ?? false,
|
|
489
|
+
triggeredFor
|
|
490
|
+
});
|
|
491
|
+
return {
|
|
492
|
+
id: webhook.id,
|
|
493
|
+
secret: webhook.secret
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
async request(path) {
|
|
497
|
+
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
498
|
+
headers: {
|
|
499
|
+
"Content-Type": "application/json",
|
|
500
|
+
"X-Api-Key": this.apiKey
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
if (!response.ok) {
|
|
504
|
+
const message = await safeReadError(response);
|
|
505
|
+
throw new Error(`Fathom API error (${response.status}): ${message}`);
|
|
506
|
+
}
|
|
507
|
+
return await response.json();
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// src/impls/fireflies-meeting-recorder.queries.ts
|
|
512
|
+
var TRANSCRIPTS_QUERY = `
|
|
513
|
+
query Transcripts(
|
|
514
|
+
$limit: Int
|
|
515
|
+
$skip: Int
|
|
516
|
+
$fromDate: DateTime
|
|
517
|
+
$toDate: DateTime
|
|
518
|
+
$keyword: String
|
|
519
|
+
$scope: TranscriptsQueryScope
|
|
520
|
+
) {
|
|
521
|
+
transcripts(
|
|
522
|
+
limit: $limit
|
|
523
|
+
skip: $skip
|
|
524
|
+
fromDate: $fromDate
|
|
525
|
+
toDate: $toDate
|
|
526
|
+
keyword: $keyword
|
|
527
|
+
scope: $scope
|
|
528
|
+
) {
|
|
529
|
+
id
|
|
530
|
+
title
|
|
531
|
+
organizer_email
|
|
532
|
+
participants
|
|
533
|
+
meeting_attendees {
|
|
534
|
+
name
|
|
535
|
+
email
|
|
536
|
+
displayName
|
|
537
|
+
}
|
|
538
|
+
dateString
|
|
539
|
+
duration
|
|
540
|
+
meeting_link
|
|
541
|
+
transcript_url
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
`;
|
|
545
|
+
var TRANSCRIPT_QUERY = `
|
|
546
|
+
query Transcript($transcriptId: String!) {
|
|
547
|
+
transcript(id: $transcriptId) {
|
|
548
|
+
id
|
|
549
|
+
title
|
|
550
|
+
organizer_email
|
|
551
|
+
participants
|
|
552
|
+
meeting_attendees {
|
|
553
|
+
name
|
|
554
|
+
email
|
|
555
|
+
displayName
|
|
556
|
+
}
|
|
557
|
+
dateString
|
|
558
|
+
duration
|
|
559
|
+
meeting_link
|
|
560
|
+
transcript_url
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
`;
|
|
564
|
+
var TRANSCRIPT_WITH_SEGMENTS_QUERY = `
|
|
565
|
+
query Transcript($transcriptId: String!) {
|
|
566
|
+
transcript(id: $transcriptId) {
|
|
567
|
+
id
|
|
568
|
+
title
|
|
569
|
+
organizer_email
|
|
570
|
+
participants
|
|
571
|
+
meeting_attendees {
|
|
572
|
+
name
|
|
573
|
+
email
|
|
574
|
+
displayName
|
|
575
|
+
}
|
|
576
|
+
dateString
|
|
577
|
+
duration
|
|
578
|
+
meeting_link
|
|
579
|
+
transcript_url
|
|
580
|
+
sentences {
|
|
581
|
+
index
|
|
582
|
+
speaker_name
|
|
583
|
+
speaker_id
|
|
584
|
+
text
|
|
585
|
+
start_time
|
|
586
|
+
end_time
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
`;
|
|
591
|
+
|
|
592
|
+
// src/impls/fireflies-meeting-recorder.utils.ts
|
|
593
|
+
import { Buffer as Buffer2 } from "node:buffer";
|
|
594
|
+
import { timingSafeEqual } from "crypto";
|
|
595
|
+
function parseSeconds(value) {
|
|
596
|
+
if (value == null)
|
|
597
|
+
return;
|
|
598
|
+
const num = typeof value === "number" ? value : Number(value);
|
|
599
|
+
if (!Number.isFinite(num))
|
|
600
|
+
return;
|
|
601
|
+
return num * 1000;
|
|
602
|
+
}
|
|
603
|
+
function normalizeHeader(headers, key) {
|
|
604
|
+
const header = headers[key] ?? headers[key.toLowerCase()] ?? headers[key.toUpperCase()];
|
|
605
|
+
if (Array.isArray(header))
|
|
606
|
+
return header[0];
|
|
607
|
+
return header;
|
|
608
|
+
}
|
|
609
|
+
function safeCompareHex(a, b) {
|
|
610
|
+
try {
|
|
611
|
+
const aBuffer = Buffer2.from(a, "hex");
|
|
612
|
+
const bBuffer = Buffer2.from(b, "hex");
|
|
613
|
+
if (aBuffer.length !== bBuffer.length)
|
|
614
|
+
return false;
|
|
615
|
+
return timingSafeEqual(aBuffer, bBuffer);
|
|
616
|
+
} catch {
|
|
617
|
+
return false;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
async function safeReadError2(response) {
|
|
621
|
+
try {
|
|
622
|
+
const data = await response.json();
|
|
623
|
+
return data?.message ?? response.statusText;
|
|
624
|
+
} catch {
|
|
625
|
+
return response.statusText;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// src/impls/fireflies-meeting-recorder.ts
|
|
630
|
+
import { createHmac } from "crypto";
|
|
631
|
+
var DEFAULT_BASE_URL2 = "https://api.fireflies.ai/graphql";
|
|
632
|
+
|
|
633
|
+
class FirefliesMeetingRecorderProvider {
|
|
634
|
+
apiKey;
|
|
635
|
+
baseUrl;
|
|
636
|
+
defaultPageSize;
|
|
637
|
+
webhookSecret;
|
|
638
|
+
constructor(options) {
|
|
639
|
+
this.apiKey = options.apiKey;
|
|
640
|
+
this.baseUrl = options.baseUrl ?? DEFAULT_BASE_URL2;
|
|
641
|
+
this.defaultPageSize = options.pageSize;
|
|
642
|
+
this.webhookSecret = options.webhookSecret;
|
|
643
|
+
}
|
|
644
|
+
async listMeetings(params) {
|
|
645
|
+
const limit = params.pageSize ?? this.defaultPageSize ?? 25;
|
|
646
|
+
const skip = params.cursor ? Number(params.cursor) : 0;
|
|
647
|
+
const data = await this.query(TRANSCRIPTS_QUERY, {
|
|
648
|
+
limit,
|
|
649
|
+
skip: Number.isFinite(skip) ? skip : 0,
|
|
650
|
+
fromDate: params.from,
|
|
651
|
+
toDate: params.to,
|
|
652
|
+
keyword: params.query,
|
|
653
|
+
scope: params.query ? "all" : undefined
|
|
654
|
+
});
|
|
655
|
+
const meetings = data.transcripts.map((transcript) => this.mapTranscriptToMeeting(transcript, params));
|
|
656
|
+
const nextCursor = meetings.length === limit ? String(skip + limit) : undefined;
|
|
657
|
+
return {
|
|
658
|
+
meetings,
|
|
659
|
+
nextCursor,
|
|
660
|
+
hasMore: Boolean(nextCursor)
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
async getMeeting(params) {
|
|
664
|
+
const data = await this.query(TRANSCRIPT_QUERY, {
|
|
665
|
+
transcriptId: params.meetingId
|
|
666
|
+
});
|
|
667
|
+
return this.mapTranscriptToMeeting(data.transcript, params);
|
|
668
|
+
}
|
|
669
|
+
async getTranscript(params) {
|
|
670
|
+
const data = await this.query(TRANSCRIPT_WITH_SEGMENTS_QUERY, { transcriptId: params.meetingId });
|
|
671
|
+
const transcript = data.transcript;
|
|
672
|
+
const segments = (transcript.sentences ?? []).map((segment) => this.mapSentence(segment));
|
|
673
|
+
return {
|
|
674
|
+
id: transcript.id,
|
|
675
|
+
meetingId: transcript.id,
|
|
676
|
+
tenantId: params.tenantId,
|
|
677
|
+
connectionId: params.connectionId ?? "unknown",
|
|
678
|
+
externalId: transcript.id,
|
|
679
|
+
format: "segments",
|
|
680
|
+
text: segments.map((segment) => segment.text).join(`
|
|
681
|
+
`),
|
|
682
|
+
segments,
|
|
683
|
+
generatedAt: transcript.dateString ?? undefined,
|
|
684
|
+
sourceUrl: transcript.transcript_url ?? undefined,
|
|
685
|
+
metadata: {
|
|
686
|
+
meetingLink: transcript.meeting_link,
|
|
687
|
+
durationMinutes: transcript.duration
|
|
688
|
+
},
|
|
689
|
+
raw: transcript
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
async parseWebhook(request) {
|
|
693
|
+
const payload = request.parsedBody ?? JSON.parse(request.rawBody);
|
|
694
|
+
const body = payload;
|
|
695
|
+
const verified = this.webhookSecret ? await this.verifyWebhook(request) : undefined;
|
|
696
|
+
return {
|
|
697
|
+
providerKey: "meeting-recorder.fireflies",
|
|
698
|
+
eventType: body.eventType,
|
|
699
|
+
meetingId: body.meetingId,
|
|
700
|
+
transcriptId: body.meetingId,
|
|
701
|
+
verified,
|
|
702
|
+
payload,
|
|
703
|
+
metadata: {
|
|
704
|
+
clientReferenceId: body.clientReferenceId
|
|
705
|
+
}
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
async verifyWebhook(request) {
|
|
709
|
+
if (!this.webhookSecret)
|
|
710
|
+
return true;
|
|
711
|
+
const signatureHeader = normalizeHeader(request.headers, "x-hub-signature");
|
|
712
|
+
if (!signatureHeader)
|
|
713
|
+
return false;
|
|
714
|
+
const signature = signatureHeader.replace(/^sha256=/, "");
|
|
715
|
+
const digest = createHmac("sha256", this.webhookSecret).update(request.rawBody).digest("hex");
|
|
716
|
+
return safeCompareHex(digest, signature);
|
|
717
|
+
}
|
|
718
|
+
mapTranscriptToMeeting(transcript, params) {
|
|
719
|
+
const connectionId = params.connectionId ?? "unknown";
|
|
720
|
+
const organizer = transcript.organizer_email ? { email: transcript.organizer_email, role: "organizer" } : undefined;
|
|
721
|
+
const attendees = transcript.meeting_attendees?.length ? transcript.meeting_attendees.map((attendee) => this.mapAttendee(attendee)) : transcript.participants?.map((email2) => ({ email: email2, role: "attendee" }));
|
|
722
|
+
return {
|
|
723
|
+
id: transcript.id,
|
|
724
|
+
tenantId: params.tenantId,
|
|
725
|
+
connectionId,
|
|
726
|
+
externalId: transcript.id,
|
|
727
|
+
title: transcript.title ?? undefined,
|
|
728
|
+
organizer,
|
|
729
|
+
invitees: attendees,
|
|
730
|
+
participants: attendees,
|
|
731
|
+
scheduledStartAt: transcript.dateString ?? undefined,
|
|
732
|
+
recordingStartAt: transcript.dateString ?? undefined,
|
|
733
|
+
durationSeconds: transcript.duration ? transcript.duration * 60 : undefined,
|
|
734
|
+
meetingUrl: transcript.meeting_link ?? transcript.transcript_url ?? undefined,
|
|
735
|
+
transcriptAvailable: Boolean(transcript.transcript_url),
|
|
736
|
+
sourcePlatform: "fireflies",
|
|
737
|
+
metadata: {
|
|
738
|
+
transcriptUrl: transcript.transcript_url
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
mapAttendee(attendee) {
|
|
743
|
+
return {
|
|
744
|
+
name: attendee.name ?? attendee.displayName ?? undefined,
|
|
745
|
+
email: attendee.email ?? undefined,
|
|
746
|
+
role: "attendee"
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
mapSentence(segment) {
|
|
750
|
+
return {
|
|
751
|
+
index: segment.index ?? undefined,
|
|
752
|
+
speakerId: segment.speaker_id ?? undefined,
|
|
753
|
+
speakerName: segment.speaker_name ?? undefined,
|
|
754
|
+
text: segment.text,
|
|
755
|
+
startTimeMs: parseSeconds(segment.start_time),
|
|
756
|
+
endTimeMs: parseSeconds(segment.end_time)
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
async query(query, variables) {
|
|
760
|
+
const response = await fetch(this.baseUrl, {
|
|
761
|
+
method: "POST",
|
|
762
|
+
headers: {
|
|
763
|
+
"Content-Type": "application/json",
|
|
764
|
+
Authorization: `Bearer ${this.apiKey}`
|
|
765
|
+
},
|
|
766
|
+
body: JSON.stringify({ query, variables })
|
|
767
|
+
});
|
|
768
|
+
if (!response.ok) {
|
|
769
|
+
const message = await safeReadError2(response);
|
|
770
|
+
throw new Error(`Fireflies API error (${response.status}): ${message}`);
|
|
771
|
+
}
|
|
772
|
+
const result = await response.json();
|
|
773
|
+
if (result.errors?.length) {
|
|
774
|
+
throw new Error(result.errors.map((error) => error.message).join("; "));
|
|
775
|
+
}
|
|
776
|
+
if (!result.data) {
|
|
777
|
+
throw new Error("Fireflies API returned empty data payload.");
|
|
778
|
+
}
|
|
779
|
+
return result.data;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// src/impls/gcs-storage.ts
|
|
784
|
+
import { Storage } from "@google-cloud/storage";
|
|
785
|
+
|
|
786
|
+
class GoogleCloudStorageProvider {
|
|
787
|
+
storage;
|
|
788
|
+
bucketName;
|
|
789
|
+
constructor(options) {
|
|
790
|
+
this.storage = options.storage ?? new Storage(options.clientOptions ?? undefined);
|
|
791
|
+
this.bucketName = options.bucket;
|
|
792
|
+
}
|
|
793
|
+
async putObject(input) {
|
|
794
|
+
const bucketName = input.bucket ?? this.bucketName;
|
|
795
|
+
const bucket = this.storage.bucket(bucketName);
|
|
796
|
+
const file = bucket.file(input.key);
|
|
797
|
+
const buffer = toBuffer(input.data);
|
|
798
|
+
await file.save(buffer, {
|
|
799
|
+
resumable: false,
|
|
800
|
+
contentType: input.contentType,
|
|
801
|
+
metadata: input.metadata
|
|
802
|
+
});
|
|
803
|
+
if (input.makePublic) {
|
|
804
|
+
await file.makePublic();
|
|
805
|
+
}
|
|
806
|
+
const [metadata] = await file.getMetadata();
|
|
807
|
+
return toMetadata(metadata);
|
|
808
|
+
}
|
|
809
|
+
async getObject(input) {
|
|
810
|
+
const bucketName = input.bucket ?? this.bucketName;
|
|
811
|
+
const bucket = this.storage.bucket(bucketName);
|
|
812
|
+
const file = bucket.file(input.key);
|
|
813
|
+
const [exists] = await file.exists();
|
|
814
|
+
if (!exists)
|
|
815
|
+
return null;
|
|
816
|
+
const [contents] = await file.download();
|
|
817
|
+
const [metadata] = await file.getMetadata();
|
|
818
|
+
return {
|
|
819
|
+
...toMetadata(metadata),
|
|
820
|
+
data: new Uint8Array(contents)
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
async deleteObject(input) {
|
|
824
|
+
const bucketName = input.bucket ?? this.bucketName;
|
|
825
|
+
const bucket = this.storage.bucket(bucketName);
|
|
826
|
+
const file = bucket.file(input.key);
|
|
827
|
+
await file.delete({ ignoreNotFound: true });
|
|
828
|
+
}
|
|
829
|
+
async generateSignedUrl(options) {
|
|
830
|
+
const bucketName = options.bucket ?? this.bucketName;
|
|
831
|
+
const bucket = this.storage.bucket(bucketName);
|
|
832
|
+
const file = bucket.file(options.key);
|
|
833
|
+
const action = options.method === "PUT" ? "write" : "read";
|
|
834
|
+
const expires = Date.now() + options.expiresInSeconds * 1000;
|
|
835
|
+
const [url] = await file.getSignedUrl({
|
|
836
|
+
action,
|
|
837
|
+
expires,
|
|
838
|
+
contentType: options.contentType
|
|
839
|
+
});
|
|
840
|
+
return { url, expiresAt: new Date(expires) };
|
|
841
|
+
}
|
|
842
|
+
async listObjects(query) {
|
|
843
|
+
const bucketName = query.bucket ?? this.bucketName;
|
|
844
|
+
const bucket = this.storage.bucket(bucketName);
|
|
845
|
+
const [files, nextQuery, response] = await bucket.getFiles({
|
|
846
|
+
prefix: query.prefix,
|
|
847
|
+
maxResults: query.maxResults,
|
|
848
|
+
pageToken: query.pageToken
|
|
849
|
+
});
|
|
850
|
+
const nextTokenFromQuery = typeof nextQuery === "object" && nextQuery !== null && "pageToken" in nextQuery ? nextQuery.pageToken : undefined;
|
|
851
|
+
const nextTokenFromResponse = response && typeof response === "object" && "nextPageToken" in response ? response.nextPageToken : undefined;
|
|
852
|
+
return {
|
|
853
|
+
objects: files.map((file) => toMetadata(file.metadata)),
|
|
854
|
+
nextPageToken: nextTokenFromQuery ?? nextTokenFromResponse ?? undefined
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
function toBuffer(data) {
|
|
859
|
+
if (data instanceof Uint8Array) {
|
|
860
|
+
return Buffer.from(data);
|
|
861
|
+
}
|
|
862
|
+
return Buffer.from(data);
|
|
863
|
+
}
|
|
864
|
+
function toMetadata(metadata) {
|
|
865
|
+
const meta = metadata;
|
|
866
|
+
return {
|
|
867
|
+
bucket: String(meta.bucket ?? ""),
|
|
868
|
+
key: String(meta.name ?? ""),
|
|
869
|
+
sizeBytes: meta.size ? Number(meta.size) : undefined,
|
|
870
|
+
contentType: meta.contentType ? String(meta.contentType) : undefined,
|
|
871
|
+
etag: meta.etag ? String(meta.etag) : undefined,
|
|
872
|
+
checksum: meta.md5Hash ? String(meta.md5Hash) : undefined,
|
|
873
|
+
lastModified: meta.updated ? new Date(String(meta.updated)) : undefined,
|
|
874
|
+
metadata: meta.metadata
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// src/impls/gmail-inbound.ts
|
|
879
|
+
import { google } from "googleapis";
|
|
880
|
+
|
|
881
|
+
class GmailInboundProvider {
|
|
882
|
+
gmail;
|
|
883
|
+
userId;
|
|
884
|
+
includeSpamTrash;
|
|
885
|
+
auth;
|
|
886
|
+
constructor(options) {
|
|
887
|
+
this.auth = options.auth;
|
|
888
|
+
this.gmail = options.gmail ?? google.gmail({
|
|
889
|
+
version: "v1",
|
|
890
|
+
auth: options.auth
|
|
891
|
+
});
|
|
892
|
+
this.userId = options.userId ?? "me";
|
|
893
|
+
this.includeSpamTrash = options.includeSpamTrash ?? false;
|
|
894
|
+
}
|
|
895
|
+
async listThreads(query) {
|
|
896
|
+
const response = await this.gmail.users.threads.list({
|
|
897
|
+
userId: this.userId,
|
|
898
|
+
maxResults: query?.pageSize,
|
|
899
|
+
pageToken: query?.pageToken,
|
|
900
|
+
q: query?.query,
|
|
901
|
+
labelIds: query?.label ? [query.label] : undefined,
|
|
902
|
+
includeSpamTrash: this.includeSpamTrash,
|
|
903
|
+
auth: this.auth
|
|
904
|
+
});
|
|
905
|
+
const threads = await Promise.all((response.data.threads ?? []).map(async (thread) => {
|
|
906
|
+
if (!thread.id)
|
|
907
|
+
return null;
|
|
908
|
+
return this.getThread(thread.id);
|
|
909
|
+
}));
|
|
910
|
+
return threads.filter((thread) => thread !== null);
|
|
911
|
+
}
|
|
912
|
+
async getThread(threadId) {
|
|
913
|
+
const response = await this.gmail.users.threads.get({
|
|
914
|
+
id: threadId,
|
|
915
|
+
userId: this.userId,
|
|
916
|
+
format: "full",
|
|
917
|
+
auth: this.auth
|
|
918
|
+
});
|
|
919
|
+
const thread = response.data;
|
|
920
|
+
if (!thread)
|
|
921
|
+
return null;
|
|
922
|
+
const messages = thread.messages?.map((message) => this.transformMessage(message)) ?? [];
|
|
923
|
+
const participants = dedupeAddresses(messages.flatMap((message) => [
|
|
924
|
+
message.from,
|
|
925
|
+
...message.to,
|
|
926
|
+
...message.cc ?? []
|
|
927
|
+
]));
|
|
928
|
+
const firstMessage = messages[0];
|
|
929
|
+
const lastMessage = messages[messages.length - 1];
|
|
930
|
+
const updatedAt = lastMessage?.receivedAt ?? lastMessage?.sentAt ?? firstMessage?.receivedAt ?? firstMessage?.sentAt ?? new Date;
|
|
931
|
+
const labels = Array.from(new Set(messages.flatMap((message) => {
|
|
932
|
+
const labelField = message.metadata?.labelIds;
|
|
933
|
+
if (!labelField)
|
|
934
|
+
return [];
|
|
935
|
+
return labelField.split(",").map((label) => label.trim());
|
|
936
|
+
}).filter((label) => Boolean(label))));
|
|
937
|
+
return {
|
|
938
|
+
id: thread.id ?? threadId,
|
|
939
|
+
subject: messages[0]?.subject,
|
|
940
|
+
snippet: thread.snippet ?? "",
|
|
941
|
+
participants,
|
|
942
|
+
messages,
|
|
943
|
+
updatedAt,
|
|
944
|
+
labels,
|
|
945
|
+
metadata: thread.historyId ? { historyId: thread.historyId } : undefined
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
async listMessagesSince(query) {
|
|
949
|
+
const after = query.since ? Math.floor(query.since.getTime() / 1000) : undefined;
|
|
950
|
+
const q = [];
|
|
951
|
+
if (after) {
|
|
952
|
+
q.push(`after:${after}`);
|
|
953
|
+
}
|
|
954
|
+
const response = await this.gmail.users.messages.list({
|
|
955
|
+
userId: this.userId,
|
|
956
|
+
maxResults: query.pageSize,
|
|
957
|
+
pageToken: query.pageToken,
|
|
958
|
+
labelIds: query.label ? [query.label] : undefined,
|
|
959
|
+
q: q.join(" "),
|
|
960
|
+
includeSpamTrash: this.includeSpamTrash,
|
|
961
|
+
auth: this.auth
|
|
962
|
+
});
|
|
963
|
+
const messages = await Promise.all((response.data.messages ?? []).map(async (item) => {
|
|
964
|
+
if (!item.id)
|
|
965
|
+
return null;
|
|
966
|
+
const full = await this.gmail.users.messages.get({
|
|
967
|
+
userId: this.userId,
|
|
968
|
+
id: item.id,
|
|
969
|
+
format: "full",
|
|
970
|
+
auth: this.auth
|
|
971
|
+
});
|
|
972
|
+
if (!full.data)
|
|
973
|
+
return null;
|
|
974
|
+
return this.transformMessage(full.data);
|
|
975
|
+
}));
|
|
976
|
+
return {
|
|
977
|
+
messages: messages.filter((message) => message !== null),
|
|
978
|
+
nextPageToken: response.data.nextPageToken ?? undefined
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
transformMessage(message) {
|
|
982
|
+
const headers = message.payload?.headers ?? [];
|
|
983
|
+
const subject = headerValue(headers, "Subject") ?? "";
|
|
984
|
+
const from = parseAddress(headerValue(headers, "From")) ?? inferFallbackAddress("from", message.id);
|
|
985
|
+
const to = parseAddressList(headerValue(headers, "To"));
|
|
986
|
+
const cc = parseAddressList(headerValue(headers, "Cc"));
|
|
987
|
+
const bcc = parseAddressList(headerValue(headers, "Bcc"));
|
|
988
|
+
const replyTo = parseAddress(headerValue(headers, "Reply-To"));
|
|
989
|
+
const { text, html, attachments } = extractContent(message.payload);
|
|
990
|
+
const timestamp = message.internalDate ? new Date(Number(message.internalDate)) : new Date;
|
|
991
|
+
const metadata = {
|
|
992
|
+
...message.labelIds?.length ? { labelIds: message.labelIds.join(",") } : {},
|
|
993
|
+
...message.historyId ? { historyId: message.historyId } : {}
|
|
994
|
+
};
|
|
995
|
+
return {
|
|
996
|
+
id: message.id ?? "",
|
|
997
|
+
threadId: message.threadId ?? "",
|
|
998
|
+
subject,
|
|
999
|
+
from,
|
|
1000
|
+
to,
|
|
1001
|
+
cc,
|
|
1002
|
+
bcc,
|
|
1003
|
+
replyTo: replyTo ?? undefined,
|
|
1004
|
+
sentAt: timestamp,
|
|
1005
|
+
receivedAt: timestamp,
|
|
1006
|
+
textBody: text ?? undefined,
|
|
1007
|
+
htmlBody: html ?? undefined,
|
|
1008
|
+
attachments,
|
|
1009
|
+
headers: Object.fromEntries(headers.map((header) => [header.name ?? "", header.value ?? ""])),
|
|
1010
|
+
metadata: Object.keys(metadata).length > 0 ? metadata : undefined
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
function headerValue(headers, name) {
|
|
1015
|
+
const header = headers.find((candidate) => candidate.name?.toLowerCase() === name.toLowerCase());
|
|
1016
|
+
const value = header?.value;
|
|
1017
|
+
return typeof value === "string" ? value : undefined;
|
|
1018
|
+
}
|
|
1019
|
+
function parseAddress(header) {
|
|
1020
|
+
const addresses = parseAddressList(header);
|
|
1021
|
+
if (addresses.length === 0) {
|
|
1022
|
+
return null;
|
|
1023
|
+
}
|
|
1024
|
+
const firstAddress = addresses[0];
|
|
1025
|
+
return firstAddress || null;
|
|
1026
|
+
}
|
|
1027
|
+
function inferFallbackAddress(field, messageId) {
|
|
1028
|
+
const suffix = messageId ? messageId.replace(/[^\w]/g, "").slice(-8) || "unknown" : "unknown";
|
|
1029
|
+
return {
|
|
1030
|
+
email: `${field}-${suffix}@mail.local`
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
function parseAddressList(header) {
|
|
1034
|
+
if (!header)
|
|
1035
|
+
return [];
|
|
1036
|
+
return header.split(",").map((part) => part.trim()).filter(Boolean).map((value) => {
|
|
1037
|
+
const match = value.match(/^(?:"?([^"]*)"?\s)?<?([^<>]+)>?$/);
|
|
1038
|
+
if (!match) {
|
|
1039
|
+
return { email: value };
|
|
1040
|
+
}
|
|
1041
|
+
const name = match[1]?.trim();
|
|
1042
|
+
const email2 = match[2]?.trim();
|
|
1043
|
+
if (!email2) {
|
|
1044
|
+
return { email: value };
|
|
1045
|
+
}
|
|
1046
|
+
return name ? { email: email2, name } : { email: email2 };
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
function dedupeAddresses(addresses) {
|
|
1050
|
+
const map = new Map;
|
|
1051
|
+
for (const address of addresses) {
|
|
1052
|
+
if (!address)
|
|
1053
|
+
continue;
|
|
1054
|
+
map.set(address.email.toLowerCase(), address);
|
|
1055
|
+
}
|
|
1056
|
+
return Array.from(map.values());
|
|
1057
|
+
}
|
|
1058
|
+
function extractContent(payload) {
|
|
1059
|
+
if (!payload) {
|
|
1060
|
+
return { attachments: [] };
|
|
1061
|
+
}
|
|
1062
|
+
const attachments = [];
|
|
1063
|
+
const visit = (part) => {
|
|
1064
|
+
if (!part)
|
|
1065
|
+
return {};
|
|
1066
|
+
if (part.filename && part.body?.attachmentId) {
|
|
1067
|
+
attachments.push({
|
|
1068
|
+
id: part.body.attachmentId,
|
|
1069
|
+
filename: part.filename,
|
|
1070
|
+
contentType: part.mimeType ?? "application/octet-stream",
|
|
1071
|
+
sizeBytes: part.body.size ?? undefined
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
const mimeType = part.mimeType ?? "";
|
|
1075
|
+
const data = part.body?.data;
|
|
1076
|
+
if (mimeType === "text/plain" && data) {
|
|
1077
|
+
return { text: decodeBase64Url(data) };
|
|
1078
|
+
}
|
|
1079
|
+
if (mimeType === "text/html" && data) {
|
|
1080
|
+
return { html: decodeBase64Url(data) };
|
|
1081
|
+
}
|
|
1082
|
+
if (part.parts?.length) {
|
|
1083
|
+
return part.parts.reduce((acc, nested) => {
|
|
1084
|
+
const value = visit(nested);
|
|
1085
|
+
return {
|
|
1086
|
+
text: value.text ?? acc.text,
|
|
1087
|
+
html: value.html ?? acc.html
|
|
1088
|
+
};
|
|
1089
|
+
}, {});
|
|
1090
|
+
}
|
|
1091
|
+
return {};
|
|
1092
|
+
};
|
|
1093
|
+
const { text, html } = visit(payload);
|
|
1094
|
+
return { text, html, attachments };
|
|
1095
|
+
}
|
|
1096
|
+
function decodeBase64Url(data) {
|
|
1097
|
+
const normalized = data.replace(/-/g, "+").replace(/_/g, "/");
|
|
1098
|
+
const padding = normalized.length % 4;
|
|
1099
|
+
const padded = padding === 0 ? normalized : normalized + "=".repeat(4 - padding);
|
|
1100
|
+
return Buffer.from(padded, "base64").toString("utf-8");
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
// src/impls/gmail-outbound.ts
|
|
1104
|
+
import { google as google2 } from "googleapis";
|
|
1105
|
+
|
|
1106
|
+
class GmailOutboundProvider {
|
|
1107
|
+
gmail;
|
|
1108
|
+
userId;
|
|
1109
|
+
auth;
|
|
1110
|
+
constructor(options) {
|
|
1111
|
+
this.auth = options.auth;
|
|
1112
|
+
this.gmail = options.gmail ?? google2.gmail({
|
|
1113
|
+
version: "v1",
|
|
1114
|
+
auth: options.auth
|
|
1115
|
+
});
|
|
1116
|
+
this.userId = options.userId ?? "me";
|
|
1117
|
+
}
|
|
1118
|
+
async sendEmail(message) {
|
|
1119
|
+
const raw = encodeMessage(message);
|
|
1120
|
+
const response = await this.gmail.users.messages.send({
|
|
1121
|
+
userId: this.userId,
|
|
1122
|
+
requestBody: {
|
|
1123
|
+
raw
|
|
1124
|
+
},
|
|
1125
|
+
auth: this.auth
|
|
1126
|
+
});
|
|
1127
|
+
const id = response.data.id ?? "";
|
|
1128
|
+
return {
|
|
1129
|
+
id,
|
|
1130
|
+
providerMessageId: response.data.id ?? undefined,
|
|
1131
|
+
queuedAt: new Date
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
function encodeMessage(message) {
|
|
1136
|
+
const headers = [
|
|
1137
|
+
`From: ${formatAddress(message.from)}`,
|
|
1138
|
+
`To: ${message.to.map(formatAddress).join(", ")}`,
|
|
1139
|
+
`Subject: ${message.subject}`,
|
|
1140
|
+
"MIME-Version: 1.0"
|
|
1141
|
+
];
|
|
1142
|
+
if (message.cc?.length) {
|
|
1143
|
+
headers.push(`Cc: ${message.cc.map(formatAddress).join(", ")}`);
|
|
1144
|
+
}
|
|
1145
|
+
if (message.replyTo) {
|
|
1146
|
+
headers.push(`Reply-To: ${formatAddress(message.replyTo)}`);
|
|
1147
|
+
}
|
|
1148
|
+
Object.entries(message.headers ?? {}).forEach(([key, value]) => {
|
|
1149
|
+
headers.push(`${key}: ${value}`);
|
|
1150
|
+
});
|
|
1151
|
+
const attachments = message.attachments ?? [];
|
|
1152
|
+
const hasHtml = Boolean(message.htmlBody);
|
|
1153
|
+
const hasText = Boolean(message.textBody);
|
|
1154
|
+
const boundaryMain = `mixed_${Date.now()}`;
|
|
1155
|
+
const boundaryAlt = `alt_${Date.now()}`;
|
|
1156
|
+
let body = "";
|
|
1157
|
+
if (attachments.length > 0) {
|
|
1158
|
+
headers.push(`Content-Type: multipart/mixed; boundary="${boundaryMain}"`);
|
|
1159
|
+
body += `\r
|
|
1160
|
+
--${boundaryMain}\r
|
|
1161
|
+
`;
|
|
1162
|
+
body += buildAlternativePart(hasText, hasHtml, boundaryAlt, message);
|
|
1163
|
+
attachments.forEach((attachment) => {
|
|
1164
|
+
body += buildAttachmentPart(boundaryMain, attachment);
|
|
1165
|
+
});
|
|
1166
|
+
body += `\r
|
|
1167
|
+
--${boundaryMain}--`;
|
|
1168
|
+
} else if (hasText && hasHtml) {
|
|
1169
|
+
headers.push(`Content-Type: multipart/alternative; boundary="${boundaryAlt}"`);
|
|
1170
|
+
body += `\r
|
|
1171
|
+
--${boundaryAlt}\r
|
|
1172
|
+
`;
|
|
1173
|
+
body += buildTextPart('text/plain; charset="utf-8"', message.textBody || "");
|
|
1174
|
+
body += `\r
|
|
1175
|
+
--${boundaryAlt}\r
|
|
1176
|
+
`;
|
|
1177
|
+
body += buildTextPart('text/html; charset="utf-8"', message.htmlBody || "");
|
|
1178
|
+
body += `\r
|
|
1179
|
+
--${boundaryAlt}--`;
|
|
1180
|
+
} else if (hasHtml) {
|
|
1181
|
+
headers.push('Content-Type: text/html; charset="utf-8"');
|
|
1182
|
+
body += `\r
|
|
1183
|
+
\r
|
|
1184
|
+
${message.htmlBody}`;
|
|
1185
|
+
} else {
|
|
1186
|
+
headers.push('Content-Type: text/plain; charset="utf-8"');
|
|
1187
|
+
body += `\r
|
|
1188
|
+
\r
|
|
1189
|
+
${message.textBody ?? ""}`;
|
|
1190
|
+
}
|
|
1191
|
+
const mime = `${headers.join(`\r
|
|
1192
|
+
`)}${body}`;
|
|
1193
|
+
return Buffer.from(mime).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1194
|
+
}
|
|
1195
|
+
function buildAlternativePart(hasText, hasHtml, boundary, message) {
|
|
1196
|
+
let content = "";
|
|
1197
|
+
content += `Content-Type: multipart/alternative; boundary="${boundary}"\r
|
|
1198
|
+
`;
|
|
1199
|
+
content += `\r
|
|
1200
|
+
`;
|
|
1201
|
+
if (hasText) {
|
|
1202
|
+
content += `--${boundary}\r
|
|
1203
|
+
`;
|
|
1204
|
+
content += buildTextPart('text/plain; charset="utf-8"', message.textBody || "");
|
|
1205
|
+
}
|
|
1206
|
+
if (hasHtml) {
|
|
1207
|
+
content += `\r
|
|
1208
|
+
--${boundary}\r
|
|
1209
|
+
`;
|
|
1210
|
+
content += buildTextPart('text/html; charset="utf-8"', message.htmlBody || "");
|
|
1211
|
+
}
|
|
1212
|
+
content += `\r
|
|
1213
|
+
--${boundary}--`;
|
|
1214
|
+
return content;
|
|
1215
|
+
}
|
|
1216
|
+
function buildTextPart(contentType, content) {
|
|
1217
|
+
return `Content-Type: ${contentType}\r
|
|
1218
|
+
` + `Content-Transfer-Encoding: 7bit\r
|
|
1219
|
+
\r
|
|
1220
|
+
` + content;
|
|
1221
|
+
}
|
|
1222
|
+
function buildAttachmentPart(boundary, attachment) {
|
|
1223
|
+
const data = attachment.data ?? new Uint8Array;
|
|
1224
|
+
const encoded = data.byteLength > 0 ? Buffer.from(data).toString("base64") : "";
|
|
1225
|
+
return `\r
|
|
1226
|
+
--${boundary}\r
|
|
1227
|
+
` + `Content-Type: ${attachment.contentType}; name="${attachment.filename}"\r
|
|
1228
|
+
` + `Content-Transfer-Encoding: base64\r
|
|
1229
|
+
` + `Content-Disposition: attachment; filename="${attachment.filename}"\r
|
|
1230
|
+
\r
|
|
1231
|
+
` + encoded;
|
|
1232
|
+
}
|
|
1233
|
+
function formatAddress(address) {
|
|
1234
|
+
if (address.name) {
|
|
1235
|
+
return `"${address.name}" <${address.email}>`;
|
|
1236
|
+
}
|
|
1237
|
+
return address.email;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
// src/impls/google-calendar.ts
|
|
1241
|
+
import { google as google3 } from "googleapis";
|
|
1242
|
+
|
|
1243
|
+
class GoogleCalendarProvider {
|
|
1244
|
+
calendar;
|
|
1245
|
+
defaultCalendarId;
|
|
1246
|
+
auth;
|
|
1247
|
+
constructor(options) {
|
|
1248
|
+
this.auth = options.auth;
|
|
1249
|
+
this.calendar = options.calendar ?? google3.calendar({
|
|
1250
|
+
version: "v3",
|
|
1251
|
+
auth: options.auth
|
|
1252
|
+
});
|
|
1253
|
+
this.defaultCalendarId = options.calendarId ?? "primary";
|
|
1254
|
+
}
|
|
1255
|
+
async listEvents(query) {
|
|
1256
|
+
const response = await this.calendar.events.list({
|
|
1257
|
+
calendarId: query.calendarId ?? this.defaultCalendarId,
|
|
1258
|
+
timeMin: query.timeMin?.toISOString(),
|
|
1259
|
+
timeMax: query.timeMax?.toISOString(),
|
|
1260
|
+
maxResults: query.maxResults,
|
|
1261
|
+
pageToken: query.pageToken,
|
|
1262
|
+
singleEvents: true,
|
|
1263
|
+
orderBy: "startTime",
|
|
1264
|
+
auth: this.auth
|
|
1265
|
+
});
|
|
1266
|
+
const events = response.data.items?.map((item) => this.fromGoogleEvent(query.calendarId ?? this.defaultCalendarId, item)) ?? [];
|
|
1267
|
+
return {
|
|
1268
|
+
events,
|
|
1269
|
+
nextPageToken: response.data.nextPageToken ?? undefined
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1272
|
+
async createEvent(input) {
|
|
1273
|
+
const calendarId = input.calendarId ?? this.defaultCalendarId;
|
|
1274
|
+
const response = await this.calendar.events.insert({
|
|
1275
|
+
calendarId,
|
|
1276
|
+
requestBody: this.toGoogleEvent(input),
|
|
1277
|
+
conferenceDataVersion: input.conference?.create ? 1 : undefined,
|
|
1278
|
+
auth: this.auth
|
|
1279
|
+
});
|
|
1280
|
+
return this.fromGoogleEvent(calendarId, response.data);
|
|
1281
|
+
}
|
|
1282
|
+
async updateEvent(calendarId, eventId, input) {
|
|
1283
|
+
const response = await this.calendar.events.patch({
|
|
1284
|
+
calendarId: calendarId ?? this.defaultCalendarId,
|
|
1285
|
+
eventId,
|
|
1286
|
+
requestBody: this.toGoogleEvent(input),
|
|
1287
|
+
conferenceDataVersion: input.conference?.create ? 1 : undefined,
|
|
1288
|
+
auth: this.auth
|
|
1289
|
+
});
|
|
1290
|
+
return this.fromGoogleEvent(calendarId, response.data);
|
|
1291
|
+
}
|
|
1292
|
+
async deleteEvent(calendarId, eventId) {
|
|
1293
|
+
await this.calendar.events.delete({
|
|
1294
|
+
calendarId: calendarId ?? this.defaultCalendarId,
|
|
1295
|
+
eventId,
|
|
1296
|
+
auth: this.auth
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
fromGoogleEvent(calendarId, event) {
|
|
1300
|
+
const start = parseDateTime(event.start);
|
|
1301
|
+
const end = parseDateTime(event.end);
|
|
1302
|
+
const attendees = event.attendees?.map((attendee) => ({
|
|
1303
|
+
email: attendee.email ?? "",
|
|
1304
|
+
name: attendee.displayName ?? undefined,
|
|
1305
|
+
optional: attendee.optional ?? undefined,
|
|
1306
|
+
responseStatus: normalizeResponseStatus(attendee.responseStatus)
|
|
1307
|
+
})) ?? [];
|
|
1308
|
+
const reminders = event.reminders?.overrides?.map((reminder) => ({
|
|
1309
|
+
method: reminder.method ?? "popup",
|
|
1310
|
+
minutesBeforeStart: reminder.minutes ?? 0
|
|
1311
|
+
})) ?? [];
|
|
1312
|
+
const metadata = buildMetadata(event);
|
|
1313
|
+
return {
|
|
1314
|
+
id: event.id ?? "",
|
|
1315
|
+
calendarId,
|
|
1316
|
+
title: event.summary ?? "",
|
|
1317
|
+
description: event.description ?? undefined,
|
|
1318
|
+
location: event.location ?? undefined,
|
|
1319
|
+
start,
|
|
1320
|
+
end,
|
|
1321
|
+
allDay: event.start?.date ? true : undefined,
|
|
1322
|
+
attendees,
|
|
1323
|
+
reminders,
|
|
1324
|
+
conferenceLink: event.hangoutLink ?? event.conferenceData?.entryPoints?.find((entry) => entry.uri)?.uri ?? undefined,
|
|
1325
|
+
metadata,
|
|
1326
|
+
createdAt: event.created ? new Date(event.created) : undefined,
|
|
1327
|
+
updatedAt: event.updated ? new Date(event.updated) : undefined
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1330
|
+
toGoogleEvent(input) {
|
|
1331
|
+
const event = {};
|
|
1332
|
+
if ("title" in input && input.title)
|
|
1333
|
+
event.summary = input.title;
|
|
1334
|
+
if (input.description !== undefined)
|
|
1335
|
+
event.description = input.description;
|
|
1336
|
+
if (input.location !== undefined)
|
|
1337
|
+
event.location = input.location;
|
|
1338
|
+
if (input.start) {
|
|
1339
|
+
event.start = formatDateTime(input.start, input.allDay);
|
|
1340
|
+
}
|
|
1341
|
+
if (input.end) {
|
|
1342
|
+
event.end = formatDateTime(input.end, input.allDay);
|
|
1343
|
+
}
|
|
1344
|
+
if (input.attendees) {
|
|
1345
|
+
event.attendees = input.attendees.map((attendee) => ({
|
|
1346
|
+
email: attendee.email,
|
|
1347
|
+
displayName: attendee.name,
|
|
1348
|
+
optional: attendee.optional,
|
|
1349
|
+
responseStatus: attendee.responseStatus
|
|
1350
|
+
}));
|
|
1351
|
+
}
|
|
1352
|
+
if (input.reminders) {
|
|
1353
|
+
event.reminders = {
|
|
1354
|
+
useDefault: false,
|
|
1355
|
+
overrides: input.reminders.map((reminder) => ({
|
|
1356
|
+
method: reminder.method,
|
|
1357
|
+
minutes: reminder.minutesBeforeStart
|
|
1358
|
+
}))
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
if (input.conference?.create) {
|
|
1362
|
+
event.conferenceData = {
|
|
1363
|
+
createRequest: {
|
|
1364
|
+
requestId: `conf-${Date.now()}`
|
|
1365
|
+
}
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1368
|
+
if (input.metadata) {
|
|
1369
|
+
event.extendedProperties = {
|
|
1370
|
+
...event.extendedProperties ?? {},
|
|
1371
|
+
private: {
|
|
1372
|
+
...event.extendedProperties?.private ?? {},
|
|
1373
|
+
...input.metadata
|
|
1374
|
+
}
|
|
1375
|
+
};
|
|
1376
|
+
}
|
|
1377
|
+
return event;
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
function parseDateTime(time) {
|
|
1381
|
+
if (!time)
|
|
1382
|
+
return new Date;
|
|
1383
|
+
if (time.dateTime)
|
|
1384
|
+
return new Date(time.dateTime);
|
|
1385
|
+
if (time.date)
|
|
1386
|
+
return new Date(`${time.date}T00:00:00`);
|
|
1387
|
+
return new Date;
|
|
1388
|
+
}
|
|
1389
|
+
function formatDateTime(date, allDay) {
|
|
1390
|
+
if (allDay) {
|
|
1391
|
+
return { date: date.toISOString().slice(0, 10) };
|
|
1392
|
+
}
|
|
1393
|
+
return { dateTime: date.toISOString() };
|
|
1394
|
+
}
|
|
1395
|
+
function normalizeResponseStatus(status) {
|
|
1396
|
+
if (!status)
|
|
1397
|
+
return;
|
|
1398
|
+
const allowed = [
|
|
1399
|
+
"needsAction",
|
|
1400
|
+
"declined",
|
|
1401
|
+
"tentative",
|
|
1402
|
+
"accepted"
|
|
1403
|
+
];
|
|
1404
|
+
return allowed.includes(status) ? status : undefined;
|
|
1405
|
+
}
|
|
1406
|
+
function buildMetadata(event) {
|
|
1407
|
+
const metadata = {};
|
|
1408
|
+
if (event.status)
|
|
1409
|
+
metadata.status = event.status;
|
|
1410
|
+
if (event.htmlLink)
|
|
1411
|
+
metadata.htmlLink = event.htmlLink;
|
|
1412
|
+
if (event.iCalUID)
|
|
1413
|
+
metadata.iCalUID = event.iCalUID;
|
|
1414
|
+
if (event.etag)
|
|
1415
|
+
metadata.etag = event.etag;
|
|
1416
|
+
if (event.conferenceData?.conferenceSolution?.name) {
|
|
1417
|
+
metadata.conferenceSolution = event.conferenceData.conferenceSolution.name;
|
|
1418
|
+
}
|
|
1419
|
+
if (event.extendedProperties?.private) {
|
|
1420
|
+
Object.entries(event.extendedProperties.private).forEach(([key, value]) => {
|
|
1421
|
+
if (typeof value === "string") {
|
|
1422
|
+
metadata[`extended.${key}`] = value;
|
|
1423
|
+
}
|
|
1424
|
+
});
|
|
1425
|
+
}
|
|
1426
|
+
return Object.keys(metadata).length > 0 ? metadata : undefined;
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
// src/impls/gradium-voice.ts
|
|
1430
|
+
import { Gradium } from "@confiture-ai/gradium-sdk-js";
|
|
1431
|
+
var FORMAT_MAP2 = {
|
|
1432
|
+
mp3: "wav",
|
|
1433
|
+
wav: "wav",
|
|
1434
|
+
ogg: "opus",
|
|
1435
|
+
pcm: "pcm"
|
|
1436
|
+
};
|
|
1437
|
+
|
|
1438
|
+
class GradiumVoiceProvider {
|
|
1439
|
+
client;
|
|
1440
|
+
defaultVoiceId;
|
|
1441
|
+
defaultOutputFormat;
|
|
1442
|
+
constructor(options) {
|
|
1443
|
+
this.client = options.client ?? new Gradium({
|
|
1444
|
+
apiKey: options.apiKey,
|
|
1445
|
+
region: options.region,
|
|
1446
|
+
baseURL: options.baseUrl,
|
|
1447
|
+
timeout: options.timeoutMs
|
|
1448
|
+
});
|
|
1449
|
+
this.defaultVoiceId = options.defaultVoiceId;
|
|
1450
|
+
this.defaultOutputFormat = options.outputFormat;
|
|
1451
|
+
}
|
|
1452
|
+
async listVoices() {
|
|
1453
|
+
const voices = await this.client.voices.list({ include_catalog: true });
|
|
1454
|
+
return voices.map((voice) => this.fromGradiumVoice(voice));
|
|
1455
|
+
}
|
|
1456
|
+
async synthesize(input) {
|
|
1457
|
+
const voiceId = input.voiceId ?? this.defaultVoiceId;
|
|
1458
|
+
if (!voiceId) {
|
|
1459
|
+
throw new Error("Voice ID is required for Gradium synthesis.");
|
|
1460
|
+
}
|
|
1461
|
+
const outputFormat = (input.format ? FORMAT_MAP2[input.format] : undefined) ?? this.defaultOutputFormat ?? "wav";
|
|
1462
|
+
const response = await this.client.tts.create({
|
|
1463
|
+
voice_id: voiceId,
|
|
1464
|
+
output_format: outputFormat,
|
|
1465
|
+
text: input.text
|
|
1466
|
+
});
|
|
1467
|
+
return {
|
|
1468
|
+
audio: response.raw_data,
|
|
1469
|
+
format: input.format ?? toContractFormat(outputFormat),
|
|
1470
|
+
sampleRateHz: input.sampleRateHz ?? response.sample_rate ?? inferSampleRate(outputFormat),
|
|
1471
|
+
durationSeconds: undefined,
|
|
1472
|
+
url: undefined
|
|
1473
|
+
};
|
|
1474
|
+
}
|
|
1475
|
+
fromGradiumVoice(voice) {
|
|
1476
|
+
return {
|
|
1477
|
+
id: voice.uid,
|
|
1478
|
+
name: voice.name,
|
|
1479
|
+
description: voice.description ?? undefined,
|
|
1480
|
+
language: voice.language ?? undefined,
|
|
1481
|
+
metadata: {
|
|
1482
|
+
startSeconds: String(voice.start_s),
|
|
1483
|
+
...voice.stop_s != null ? { stopSeconds: String(voice.stop_s) } : {},
|
|
1484
|
+
filename: voice.filename
|
|
1485
|
+
}
|
|
1486
|
+
};
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
function toContractFormat(format) {
|
|
1490
|
+
switch (format) {
|
|
1491
|
+
case "opus":
|
|
1492
|
+
return "ogg";
|
|
1493
|
+
case "wav":
|
|
1494
|
+
return "wav";
|
|
1495
|
+
case "pcm":
|
|
1496
|
+
case "pcm_16000":
|
|
1497
|
+
case "pcm_24000":
|
|
1498
|
+
return "pcm";
|
|
1499
|
+
default:
|
|
1500
|
+
return format;
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
function inferSampleRate(format) {
|
|
1504
|
+
switch (format) {
|
|
1505
|
+
case "ulaw_8000":
|
|
1506
|
+
case "alaw_8000":
|
|
1507
|
+
return 8000;
|
|
1508
|
+
case "pcm_16000":
|
|
1509
|
+
return 16000;
|
|
1510
|
+
case "pcm_24000":
|
|
1511
|
+
return 24000;
|
|
1512
|
+
default:
|
|
1513
|
+
return 48000;
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
// src/impls/granola-meeting-recorder.mcp.ts
|
|
1518
|
+
var UNKNOWN_EMAIL = "unknown@granola.local";
|
|
1519
|
+
var EPOCH = "1970-01-01T00:00:00.000Z";
|
|
1520
|
+
|
|
1521
|
+
class GranolaMcpClient {
|
|
1522
|
+
requestId = 0;
|
|
1523
|
+
mcpUrl;
|
|
1524
|
+
mcpAccessToken;
|
|
1525
|
+
mcpHeaders;
|
|
1526
|
+
fetchFn;
|
|
1527
|
+
constructor(options) {
|
|
1528
|
+
this.mcpUrl = options.mcpUrl;
|
|
1529
|
+
this.mcpAccessToken = options.mcpAccessToken;
|
|
1530
|
+
this.mcpHeaders = options.mcpHeaders;
|
|
1531
|
+
this.fetchFn = options.fetchFn ?? fetch;
|
|
1532
|
+
}
|
|
1533
|
+
async callTool(name, args) {
|
|
1534
|
+
const headers = {
|
|
1535
|
+
"Content-Type": "application/json",
|
|
1536
|
+
...this.mcpHeaders ?? {}
|
|
1537
|
+
};
|
|
1538
|
+
if (this.mcpAccessToken) {
|
|
1539
|
+
headers.Authorization = `Bearer ${this.mcpAccessToken}`;
|
|
1540
|
+
}
|
|
1541
|
+
const response = await this.fetchFn(this.mcpUrl, {
|
|
1542
|
+
method: "POST",
|
|
1543
|
+
headers,
|
|
1544
|
+
body: JSON.stringify({
|
|
1545
|
+
jsonrpc: "2.0",
|
|
1546
|
+
id: ++this.requestId,
|
|
1547
|
+
method: "tools/call",
|
|
1548
|
+
params: {
|
|
1549
|
+
name,
|
|
1550
|
+
arguments: args
|
|
1551
|
+
}
|
|
1552
|
+
})
|
|
1553
|
+
});
|
|
1554
|
+
if (!response.ok) {
|
|
1555
|
+
const message = await safeReadText(response);
|
|
1556
|
+
throw new Error(`Granola MCP error (${response.status}): ${message}`);
|
|
1557
|
+
}
|
|
1558
|
+
const rpc = await response.json();
|
|
1559
|
+
if (rpc.error) {
|
|
1560
|
+
throw new Error(rpc.error.message ?? "Granola MCP returned an error.");
|
|
1561
|
+
}
|
|
1562
|
+
return extractRpcResult(rpc);
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
function normalizeMcpListResult(payload) {
|
|
1566
|
+
const root = asObject(payload);
|
|
1567
|
+
const list = asArray(payload) ?? asArray(root?.notes) ?? asArray(root?.meetings) ?? asArray(root?.items) ?? asArray(root?.results) ?? asArray(root?.data) ?? [];
|
|
1568
|
+
const notes = list.map((item) => mapSummaryItem(item)).filter((item) => Boolean(item));
|
|
1569
|
+
return {
|
|
1570
|
+
notes,
|
|
1571
|
+
nextCursor: readString(root, ["nextCursor", "next_cursor", "cursor"]),
|
|
1572
|
+
hasMore: readBoolean(root, ["hasMore", "has_more"])
|
|
1573
|
+
};
|
|
1574
|
+
}
|
|
1575
|
+
function normalizeMcpMeeting(payload, targetMeetingId) {
|
|
1576
|
+
const root = asObject(payload);
|
|
1577
|
+
const candidates = asArray(payload) ?? asArray(root?.meetings) ?? asArray(root?.notes) ?? asArray(root?.items) ?? asArray(root?.results) ?? asArray(root?.data);
|
|
1578
|
+
if (candidates?.length) {
|
|
1579
|
+
const selected = candidates.find((item) => readId(item) === targetMeetingId) ?? candidates.find((item) => String(readId(item) ?? "").includes(targetMeetingId)) ?? candidates[0];
|
|
1580
|
+
return selected ? mapMeetingItem(selected) : undefined;
|
|
1581
|
+
}
|
|
1582
|
+
const direct = mapMeetingItem(payload);
|
|
1583
|
+
if (direct && direct.id === targetMeetingId) {
|
|
1584
|
+
return direct;
|
|
1585
|
+
}
|
|
1586
|
+
return direct;
|
|
1587
|
+
}
|
|
1588
|
+
function normalizeMcpTranscript(payload) {
|
|
1589
|
+
const root = asObject(payload);
|
|
1590
|
+
const list = asArray(payload) ?? asArray(root?.transcript) ?? asArray(root?.segments) ?? asArray(root?.items) ?? asArray(root?.data) ?? [];
|
|
1591
|
+
if (list.length === 0 && typeof payload === "string") {
|
|
1592
|
+
return [
|
|
1593
|
+
{
|
|
1594
|
+
text: payload,
|
|
1595
|
+
start_time: "00:00:00",
|
|
1596
|
+
end_time: "00:00:00"
|
|
1597
|
+
}
|
|
1598
|
+
];
|
|
1599
|
+
}
|
|
1600
|
+
return list.map((item) => mapTranscriptSegment2(item)).filter((item) => Boolean(item));
|
|
1601
|
+
}
|
|
1602
|
+
function extractRpcResult(rpc) {
|
|
1603
|
+
const result = rpc.result;
|
|
1604
|
+
if (!result)
|
|
1605
|
+
return null;
|
|
1606
|
+
if (result.structuredContent !== undefined)
|
|
1607
|
+
return result.structuredContent;
|
|
1608
|
+
if (result.data !== undefined)
|
|
1609
|
+
return result.data;
|
|
1610
|
+
const textPayload = result.content?.find((entry) => entry?.type === "text")?.text;
|
|
1611
|
+
if (!textPayload)
|
|
1612
|
+
return result;
|
|
1613
|
+
try {
|
|
1614
|
+
return JSON.parse(textPayload);
|
|
1615
|
+
} catch {
|
|
1616
|
+
return textPayload;
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
function mapSummaryItem(value) {
|
|
1620
|
+
const object = asObject(value);
|
|
1621
|
+
if (!object)
|
|
1622
|
+
return;
|
|
1623
|
+
const id = readId(object);
|
|
1624
|
+
if (!id)
|
|
1625
|
+
return;
|
|
1626
|
+
const owner = mapOwner(object);
|
|
1627
|
+
return {
|
|
1628
|
+
id,
|
|
1629
|
+
title: readString(object, ["title", "name", "meeting_title"]) ?? null,
|
|
1630
|
+
owner,
|
|
1631
|
+
created_at: readString(object, ["created_at", "createdAt", "date", "meeting_date"]) ?? EPOCH
|
|
1632
|
+
};
|
|
1633
|
+
}
|
|
1634
|
+
function mapMeetingItem(value) {
|
|
1635
|
+
const summary = mapSummaryItem(value);
|
|
1636
|
+
if (!summary)
|
|
1637
|
+
return;
|
|
1638
|
+
const object = asObject(value) ?? {};
|
|
1639
|
+
const attendees = asArray(object.attendees) ?? asArray(object.participants) ?? asArray(object.invitees) ?? [];
|
|
1640
|
+
const mappedAttendees = attendees.map((entry) => mapUser(entry)).filter((entry) => Boolean(entry));
|
|
1641
|
+
const folders = asArray(object.folder_membership) ?? asArray(object.folders) ?? [];
|
|
1642
|
+
const folderMembership = folders.map((entry, index) => mapFolder(entry, index)).filter((entry) => Boolean(entry));
|
|
1643
|
+
const calendarEvent = asObject(object.calendar_event) ? {
|
|
1644
|
+
event_title: readString(asObject(object.calendar_event), [
|
|
1645
|
+
"event_title",
|
|
1646
|
+
"title"
|
|
1647
|
+
]) ?? null,
|
|
1648
|
+
invitees: mappedAttendees.map((attendee) => ({
|
|
1649
|
+
email: attendee.email
|
|
1650
|
+
})),
|
|
1651
|
+
organiser: readString(asObject(object.calendar_event), [
|
|
1652
|
+
"organiser",
|
|
1653
|
+
"organizer"
|
|
1654
|
+
]) ?? summary.owner.email,
|
|
1655
|
+
calendar_event_id: readString(asObject(object.calendar_event), [
|
|
1656
|
+
"calendar_event_id",
|
|
1657
|
+
"id"
|
|
1658
|
+
]) ?? null,
|
|
1659
|
+
scheduled_start_time: readString(asObject(object.calendar_event), [
|
|
1660
|
+
"scheduled_start_time",
|
|
1661
|
+
"start_time",
|
|
1662
|
+
"start"
|
|
1663
|
+
]) ?? null,
|
|
1664
|
+
scheduled_end_time: readString(asObject(object.calendar_event), [
|
|
1665
|
+
"scheduled_end_time",
|
|
1666
|
+
"end_time",
|
|
1667
|
+
"end"
|
|
1668
|
+
]) ?? null
|
|
1669
|
+
} : null;
|
|
1670
|
+
const transcript = normalizeMcpTranscript(object.transcript ?? object.segments);
|
|
1671
|
+
return {
|
|
1672
|
+
...summary,
|
|
1673
|
+
calendar_event: calendarEvent,
|
|
1674
|
+
attendees: mappedAttendees,
|
|
1675
|
+
folder_membership: folderMembership,
|
|
1676
|
+
summary_text: readString(object, ["summary_text", "summary", "enhanced_notes"]) ?? "",
|
|
1677
|
+
transcript: transcript.length ? transcript : null
|
|
1678
|
+
};
|
|
1679
|
+
}
|
|
1680
|
+
function mapTranscriptSegment2(value) {
|
|
1681
|
+
const object = asObject(value);
|
|
1682
|
+
if (!object) {
|
|
1683
|
+
if (typeof value !== "string")
|
|
1684
|
+
return;
|
|
1685
|
+
return {
|
|
1686
|
+
text: value,
|
|
1687
|
+
start_time: "00:00:00",
|
|
1688
|
+
end_time: "00:00:00"
|
|
1689
|
+
};
|
|
1690
|
+
}
|
|
1691
|
+
const text = readString(object, ["text", "content", "utterance"]);
|
|
1692
|
+
if (!text)
|
|
1693
|
+
return;
|
|
1694
|
+
const speakerSource = readString(asObject(object.speaker), ["source", "name"]) ?? readString(object, ["speaker", "speaker_name"]);
|
|
1695
|
+
return {
|
|
1696
|
+
speaker: speakerSource ? { source: speakerSource } : undefined,
|
|
1697
|
+
text,
|
|
1698
|
+
start_time: readString(object, ["start_time", "startTime", "timestamp", "time"]) ?? "00:00:00",
|
|
1699
|
+
end_time: readString(object, ["end_time", "endTime"]) ?? "00:00:00"
|
|
1700
|
+
};
|
|
1701
|
+
}
|
|
1702
|
+
function mapOwner(object) {
|
|
1703
|
+
const ownerObject = asObject(object.owner) ?? asObject(object.organizer) ?? asObject(object.organiser);
|
|
1704
|
+
const attendee = asArray(object.attendees)?.[0] ?? asArray(object.participants)?.[0] ?? asArray(object.invitees)?.[0];
|
|
1705
|
+
const ownerCandidate = ownerObject ?? asObject(attendee) ?? {};
|
|
1706
|
+
return {
|
|
1707
|
+
name: readString(ownerCandidate, ["name", "displayName"]) ?? null,
|
|
1708
|
+
email: readString(ownerCandidate, ["email"]) ?? UNKNOWN_EMAIL
|
|
1709
|
+
};
|
|
1710
|
+
}
|
|
1711
|
+
function mapUser(value) {
|
|
1712
|
+
if (typeof value === "string") {
|
|
1713
|
+
return {
|
|
1714
|
+
name: null,
|
|
1715
|
+
email: value
|
|
1716
|
+
};
|
|
1717
|
+
}
|
|
1718
|
+
const object = asObject(value);
|
|
1719
|
+
if (!object)
|
|
1720
|
+
return;
|
|
1721
|
+
const email2 = readString(object, ["email"]);
|
|
1722
|
+
if (!email2)
|
|
1723
|
+
return;
|
|
1724
|
+
return {
|
|
1725
|
+
name: readString(object, ["name", "displayName"]) ?? null,
|
|
1726
|
+
email: email2
|
|
1727
|
+
};
|
|
1728
|
+
}
|
|
1729
|
+
function mapFolder(value, index) {
|
|
1730
|
+
const object = asObject(value);
|
|
1731
|
+
if (!object)
|
|
1732
|
+
return;
|
|
1733
|
+
const id = readString(object, ["id"]) ?? `folder-${index}`;
|
|
1734
|
+
const name = readString(object, ["name"]) ?? "Folder";
|
|
1735
|
+
return {
|
|
1736
|
+
id,
|
|
1737
|
+
object: "folder",
|
|
1738
|
+
name
|
|
1739
|
+
};
|
|
1740
|
+
}
|
|
1741
|
+
function readId(value) {
|
|
1742
|
+
const object = asObject(value);
|
|
1743
|
+
if (!object)
|
|
1744
|
+
return;
|
|
1745
|
+
return readString(object, [
|
|
1746
|
+
"id",
|
|
1747
|
+
"meeting_id",
|
|
1748
|
+
"meetingId",
|
|
1749
|
+
"note_id",
|
|
1750
|
+
"noteId"
|
|
1751
|
+
]);
|
|
1752
|
+
}
|
|
1753
|
+
function readString(object, keys) {
|
|
1754
|
+
if (!object)
|
|
1755
|
+
return;
|
|
1756
|
+
for (const key of keys) {
|
|
1757
|
+
const value = object[key];
|
|
1758
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
1759
|
+
return value;
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
return;
|
|
1763
|
+
}
|
|
1764
|
+
function readBoolean(object, keys) {
|
|
1765
|
+
if (!object)
|
|
1766
|
+
return;
|
|
1767
|
+
for (const key of keys) {
|
|
1768
|
+
const value = object[key];
|
|
1769
|
+
if (typeof value === "boolean")
|
|
1770
|
+
return value;
|
|
1771
|
+
}
|
|
1772
|
+
return;
|
|
1773
|
+
}
|
|
1774
|
+
function asObject(value) {
|
|
1775
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
1776
|
+
return;
|
|
1777
|
+
return value;
|
|
1778
|
+
}
|
|
1779
|
+
function asArray(value) {
|
|
1780
|
+
return Array.isArray(value) ? value : undefined;
|
|
1781
|
+
}
|
|
1782
|
+
async function safeReadText(response) {
|
|
1783
|
+
try {
|
|
1784
|
+
return await response.text();
|
|
1785
|
+
} catch {
|
|
1786
|
+
return response.statusText;
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
// src/impls/granola-meeting-recorder.ts
|
|
1791
|
+
var DEFAULT_BASE_URL3 = "https://public-api.granola.ai";
|
|
1792
|
+
var DEFAULT_MCP_URL = "https://mcp.granola.ai/mcp";
|
|
1793
|
+
var MAX_PAGE_SIZE = 30;
|
|
1794
|
+
|
|
1795
|
+
class GranolaMeetingRecorderProvider {
|
|
1796
|
+
apiKey;
|
|
1797
|
+
baseUrl;
|
|
1798
|
+
defaultPageSize;
|
|
1799
|
+
transport;
|
|
1800
|
+
mcpClient;
|
|
1801
|
+
constructor(options) {
|
|
1802
|
+
this.apiKey = options.apiKey;
|
|
1803
|
+
this.baseUrl = options.baseUrl ?? DEFAULT_BASE_URL3;
|
|
1804
|
+
this.defaultPageSize = options.pageSize;
|
|
1805
|
+
this.transport = options.transport ?? "api";
|
|
1806
|
+
this.mcpClient = new GranolaMcpClient({
|
|
1807
|
+
mcpUrl: options.mcpUrl ?? DEFAULT_MCP_URL,
|
|
1808
|
+
mcpAccessToken: options.mcpAccessToken,
|
|
1809
|
+
mcpHeaders: options.mcpHeaders,
|
|
1810
|
+
fetchFn: options.fetchFn
|
|
1811
|
+
});
|
|
1812
|
+
}
|
|
1813
|
+
async listMeetings(params) {
|
|
1814
|
+
if (this.transport === "mcp") {
|
|
1815
|
+
return this.listMeetingsViaMcp(params);
|
|
1816
|
+
}
|
|
1817
|
+
const query = new URLSearchParams;
|
|
1818
|
+
if (params.from)
|
|
1819
|
+
query.set("created_after", params.from);
|
|
1820
|
+
if (params.to)
|
|
1821
|
+
query.set("created_before", params.to);
|
|
1822
|
+
if (params.cursor)
|
|
1823
|
+
query.set("cursor", params.cursor);
|
|
1824
|
+
const pageSize = params.pageSize ?? this.defaultPageSize;
|
|
1825
|
+
if (pageSize) {
|
|
1826
|
+
query.set("page_size", String(Math.min(pageSize, MAX_PAGE_SIZE)));
|
|
1827
|
+
}
|
|
1828
|
+
const data = await this.request(`/v1/notes?${query.toString()}`);
|
|
1829
|
+
return {
|
|
1830
|
+
meetings: data.notes.map((note) => this.mapNoteSummary(note, params)),
|
|
1831
|
+
nextCursor: data.cursor ?? undefined,
|
|
1832
|
+
hasMore: data.hasMore
|
|
1833
|
+
};
|
|
1834
|
+
}
|
|
1835
|
+
async getMeeting(params) {
|
|
1836
|
+
if (this.transport === "mcp") {
|
|
1837
|
+
return this.getMeetingViaMcp(params);
|
|
1838
|
+
}
|
|
1839
|
+
const includeTranscript = params.includeTranscript ?? false;
|
|
1840
|
+
const note = await this.getNote(params.meetingId, includeTranscript);
|
|
1841
|
+
return this.mapNoteDetail(note, params);
|
|
1842
|
+
}
|
|
1843
|
+
async getTranscript(params) {
|
|
1844
|
+
if (this.transport === "mcp") {
|
|
1845
|
+
return this.getTranscriptViaMcp(params);
|
|
1846
|
+
}
|
|
1847
|
+
const note = await this.getNote(params.meetingId, true);
|
|
1848
|
+
const segments = this.mapTranscriptSegments(note.transcript);
|
|
1849
|
+
return {
|
|
1850
|
+
id: note.id,
|
|
1851
|
+
meetingId: note.id,
|
|
1852
|
+
tenantId: params.tenantId,
|
|
1853
|
+
connectionId: params.connectionId ?? "unknown",
|
|
1854
|
+
externalId: note.id,
|
|
1855
|
+
format: "segments",
|
|
1856
|
+
text: segments.map((segment) => segment.text).join(`
|
|
1857
|
+
`),
|
|
1858
|
+
segments,
|
|
1859
|
+
generatedAt: note.created_at,
|
|
1860
|
+
metadata: {
|
|
1861
|
+
summaryText: note.summary_text
|
|
1862
|
+
},
|
|
1863
|
+
raw: note.transcript ?? undefined
|
|
1864
|
+
};
|
|
1865
|
+
}
|
|
1866
|
+
async listMeetingsViaMcp(params) {
|
|
1867
|
+
const payload = await this.mcpClient.callTool("list_meetings", {
|
|
1868
|
+
cursor: params.cursor,
|
|
1869
|
+
limit: params.pageSize ?? this.defaultPageSize,
|
|
1870
|
+
query: params.query,
|
|
1871
|
+
from: params.from,
|
|
1872
|
+
to: params.to,
|
|
1873
|
+
organizerEmail: params.organizerEmail,
|
|
1874
|
+
participantEmail: params.participantEmail
|
|
1875
|
+
});
|
|
1876
|
+
const normalized = normalizeMcpListResult(payload);
|
|
1877
|
+
return {
|
|
1878
|
+
meetings: normalized.notes.map((note) => this.mapNoteSummary(note, params)),
|
|
1879
|
+
nextCursor: normalized.nextCursor,
|
|
1880
|
+
hasMore: normalized.hasMore ?? Boolean(normalized.nextCursor && normalized.notes.length > 0)
|
|
1881
|
+
};
|
|
1882
|
+
}
|
|
1883
|
+
async getMeetingViaMcp(params) {
|
|
1884
|
+
const primaryPayload = await this.mcpClient.callTool("list_meetings", {
|
|
1885
|
+
query: params.meetingId,
|
|
1886
|
+
limit: 50
|
|
1887
|
+
});
|
|
1888
|
+
let note = normalizeMcpMeeting(primaryPayload, params.meetingId);
|
|
1889
|
+
if (!note) {
|
|
1890
|
+
const fallbackPayload = await this.mcpClient.callTool("get_meetings", {
|
|
1891
|
+
query: params.meetingId
|
|
1892
|
+
});
|
|
1893
|
+
note = normalizeMcpMeeting(fallbackPayload, params.meetingId);
|
|
1894
|
+
}
|
|
1895
|
+
if (!note) {
|
|
1896
|
+
throw new Error(`Granola meeting "${params.meetingId}" not found via MCP.`);
|
|
1897
|
+
}
|
|
1898
|
+
return this.mapNoteDetail(note, params);
|
|
1899
|
+
}
|
|
1900
|
+
async getTranscriptViaMcp(params) {
|
|
1901
|
+
const payload = await this.mcpClient.callTool("get_meeting_transcript", {
|
|
1902
|
+
meeting_id: params.meetingId,
|
|
1903
|
+
meetingId: params.meetingId,
|
|
1904
|
+
id: params.meetingId
|
|
1905
|
+
});
|
|
1906
|
+
const transcript = normalizeMcpTranscript(payload);
|
|
1907
|
+
const segments = this.mapTranscriptSegments(transcript);
|
|
1908
|
+
return {
|
|
1909
|
+
id: params.meetingId,
|
|
1910
|
+
meetingId: params.meetingId,
|
|
1911
|
+
tenantId: params.tenantId,
|
|
1912
|
+
connectionId: params.connectionId ?? "unknown",
|
|
1913
|
+
externalId: params.meetingId,
|
|
1914
|
+
format: "segments",
|
|
1915
|
+
text: segments.map((segment) => segment.text).join(`
|
|
1916
|
+
`),
|
|
1917
|
+
segments,
|
|
1918
|
+
metadata: {
|
|
1919
|
+
provider: "granola",
|
|
1920
|
+
transport: "mcp"
|
|
1921
|
+
},
|
|
1922
|
+
raw: payload
|
|
1923
|
+
};
|
|
1924
|
+
}
|
|
1925
|
+
async getNote(noteId, includeTranscript) {
|
|
1926
|
+
const query = includeTranscript ? "?include=transcript" : "";
|
|
1927
|
+
return this.request(`/v1/notes/${noteId}${query}`);
|
|
1928
|
+
}
|
|
1929
|
+
mapNoteSummary(note, params) {
|
|
1930
|
+
const connectionId = params.connectionId ?? "unknown";
|
|
1931
|
+
return {
|
|
1932
|
+
id: note.id,
|
|
1933
|
+
tenantId: params.tenantId,
|
|
1934
|
+
connectionId,
|
|
1935
|
+
externalId: note.id,
|
|
1936
|
+
title: note.title ?? undefined,
|
|
1937
|
+
organizer: this.mapUser(note.owner),
|
|
1938
|
+
scheduledStartAt: note.created_at,
|
|
1939
|
+
recordingStartAt: note.created_at,
|
|
1940
|
+
transcriptAvailable: false,
|
|
1941
|
+
createdAt: note.created_at,
|
|
1942
|
+
updatedAt: note.created_at,
|
|
1943
|
+
sourcePlatform: "granola"
|
|
1944
|
+
};
|
|
1945
|
+
}
|
|
1946
|
+
mapNoteDetail(note, params) {
|
|
1947
|
+
const connectionId = params.connectionId ?? "unknown";
|
|
1948
|
+
const calendarEvent = note.calendar_event ?? undefined;
|
|
1949
|
+
const invitees = calendarEvent?.invitees ? calendarEvent.invitees.map((invitee) => this.mapInvitee(invitee)) : note.attendees?.map((attendee) => this.mapUser(attendee)).filter(Boolean);
|
|
1950
|
+
const participants = note.attendees?.map((attendee) => this.mapUser(attendee)).filter(Boolean);
|
|
1951
|
+
return {
|
|
1952
|
+
id: note.id,
|
|
1953
|
+
tenantId: params.tenantId,
|
|
1954
|
+
connectionId,
|
|
1955
|
+
externalId: note.id,
|
|
1956
|
+
title: note.title ?? calendarEvent?.event_title ?? undefined,
|
|
1957
|
+
summary: note.summary_text ?? undefined,
|
|
1958
|
+
organizer: this.mapUser(note.owner),
|
|
1959
|
+
invitees: invitees?.length ? invitees : undefined,
|
|
1960
|
+
participants: participants?.length ? participants : undefined,
|
|
1961
|
+
scheduledStartAt: calendarEvent?.scheduled_start_time ?? undefined,
|
|
1962
|
+
scheduledEndAt: calendarEvent?.scheduled_end_time ?? undefined,
|
|
1963
|
+
recordingStartAt: calendarEvent?.scheduled_start_time ?? note.created_at,
|
|
1964
|
+
recordingEndAt: calendarEvent?.scheduled_end_time ?? undefined,
|
|
1965
|
+
transcriptAvailable: Array.isArray(note.transcript),
|
|
1966
|
+
createdAt: note.created_at,
|
|
1967
|
+
updatedAt: note.created_at,
|
|
1968
|
+
sourcePlatform: "granola",
|
|
1969
|
+
metadata: {
|
|
1970
|
+
calendarEvent,
|
|
1971
|
+
folderMembership: note.folder_membership
|
|
1972
|
+
}
|
|
1973
|
+
};
|
|
1974
|
+
}
|
|
1975
|
+
mapTranscriptSegments(transcript) {
|
|
1976
|
+
if (!transcript)
|
|
1977
|
+
return [];
|
|
1978
|
+
return transcript.map((segment, index) => ({
|
|
1979
|
+
index,
|
|
1980
|
+
speakerName: segment.speaker?.source ?? undefined,
|
|
1981
|
+
text: segment.text,
|
|
1982
|
+
startTime: segment.start_time,
|
|
1983
|
+
endTime: segment.end_time
|
|
1984
|
+
}));
|
|
1985
|
+
}
|
|
1986
|
+
mapUser(user) {
|
|
1987
|
+
if (!user)
|
|
1988
|
+
return;
|
|
1989
|
+
return {
|
|
1990
|
+
name: user.name ?? undefined,
|
|
1991
|
+
email: user.email ?? undefined,
|
|
1992
|
+
role: "organizer"
|
|
1993
|
+
};
|
|
1994
|
+
}
|
|
1995
|
+
mapInvitee(invitee) {
|
|
1996
|
+
return {
|
|
1997
|
+
email: invitee.email,
|
|
1998
|
+
role: "attendee"
|
|
1999
|
+
};
|
|
2000
|
+
}
|
|
2001
|
+
async request(path) {
|
|
2002
|
+
if (!this.apiKey) {
|
|
2003
|
+
throw new Error('Granola apiKey is required when transport is "api".');
|
|
2004
|
+
}
|
|
2005
|
+
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
2006
|
+
headers: {
|
|
2007
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
2008
|
+
"Content-Type": "application/json"
|
|
2009
|
+
}
|
|
2010
|
+
});
|
|
2011
|
+
if (!response.ok) {
|
|
2012
|
+
const message = await safeReadError3(response);
|
|
2013
|
+
throw new Error(`Granola API error (${response.status}): ${message}`);
|
|
2014
|
+
}
|
|
2015
|
+
return await response.json();
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
async function safeReadError3(response) {
|
|
2019
|
+
try {
|
|
2020
|
+
const data = await response.json();
|
|
2021
|
+
return data?.message ?? response.statusText;
|
|
2022
|
+
} catch {
|
|
2023
|
+
return response.statusText;
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
// src/impls/mistral-llm.ts
|
|
2028
|
+
import { Mistral } from "@mistralai/mistralai";
|
|
2029
|
+
|
|
2030
|
+
class MistralLLMProvider {
|
|
2031
|
+
client;
|
|
2032
|
+
defaultModel;
|
|
2033
|
+
constructor(options) {
|
|
2034
|
+
if (!options.apiKey) {
|
|
2035
|
+
throw new Error("MistralLLMProvider requires an apiKey");
|
|
2036
|
+
}
|
|
2037
|
+
this.client = options.client ?? new Mistral({
|
|
2038
|
+
apiKey: options.apiKey,
|
|
2039
|
+
serverURL: options.serverURL,
|
|
2040
|
+
userAgent: options.userAgentSuffix ? `${options.userAgentSuffix}` : undefined
|
|
2041
|
+
});
|
|
2042
|
+
this.defaultModel = options.defaultModel ?? "mistral-large-latest";
|
|
2043
|
+
}
|
|
2044
|
+
async chat(messages, options = {}) {
|
|
2045
|
+
const request = this.buildChatRequest(messages, options);
|
|
2046
|
+
const response = await this.client.chat.complete(request);
|
|
2047
|
+
return this.buildLLMResponse(response);
|
|
2048
|
+
}
|
|
2049
|
+
async* stream(messages, options = {}) {
|
|
2050
|
+
const request = this.buildChatRequest(messages, options);
|
|
2051
|
+
request.stream = true;
|
|
2052
|
+
const stream = await this.client.chat.stream(request);
|
|
2053
|
+
const aggregatedParts = [];
|
|
2054
|
+
const aggregatedToolCalls = [];
|
|
2055
|
+
let usage;
|
|
2056
|
+
let finishReason;
|
|
2057
|
+
for await (const event of stream) {
|
|
2058
|
+
for (const choice of event.data.choices) {
|
|
2059
|
+
const delta = choice.delta;
|
|
2060
|
+
if (typeof delta.content === "string") {
|
|
2061
|
+
if (delta.content.length > 0) {
|
|
2062
|
+
aggregatedParts.push({ type: "text", text: delta.content });
|
|
2063
|
+
yield {
|
|
2064
|
+
type: "message_delta",
|
|
2065
|
+
delta: { type: "text", text: delta.content },
|
|
2066
|
+
index: choice.index
|
|
2067
|
+
};
|
|
2068
|
+
}
|
|
2069
|
+
} else if (Array.isArray(delta.content)) {
|
|
2070
|
+
for (const chunk of delta.content) {
|
|
2071
|
+
if (chunk.type === "text" && "text" in chunk) {
|
|
2072
|
+
aggregatedParts.push({ type: "text", text: chunk.text });
|
|
2073
|
+
yield {
|
|
2074
|
+
type: "message_delta",
|
|
2075
|
+
delta: { type: "text", text: chunk.text },
|
|
2076
|
+
index: choice.index
|
|
2077
|
+
};
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
if (delta.toolCalls) {
|
|
2082
|
+
let localIndex = 0;
|
|
2083
|
+
for (const call of delta.toolCalls) {
|
|
2084
|
+
const toolCall = this.fromMistralToolCall(call, localIndex);
|
|
2085
|
+
aggregatedToolCalls.push(toolCall);
|
|
2086
|
+
yield {
|
|
2087
|
+
type: "tool_call",
|
|
2088
|
+
call: toolCall,
|
|
2089
|
+
index: choice.index
|
|
2090
|
+
};
|
|
2091
|
+
localIndex += 1;
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
if (choice.finishReason && choice.finishReason !== "null") {
|
|
2095
|
+
finishReason = choice.finishReason;
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
if (event.data.usage) {
|
|
2099
|
+
const usageEntry = this.fromUsage(event.data.usage);
|
|
2100
|
+
if (usageEntry) {
|
|
2101
|
+
usage = usageEntry;
|
|
2102
|
+
yield { type: "usage", usage: usageEntry };
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
const message = {
|
|
2107
|
+
role: "assistant",
|
|
2108
|
+
content: aggregatedParts.length ? aggregatedParts : [{ type: "text", text: "" }]
|
|
2109
|
+
};
|
|
2110
|
+
if (aggregatedToolCalls.length > 0) {
|
|
2111
|
+
message.content = [
|
|
2112
|
+
...aggregatedToolCalls,
|
|
2113
|
+
...aggregatedParts.length ? aggregatedParts : []
|
|
2114
|
+
];
|
|
2115
|
+
}
|
|
2116
|
+
yield {
|
|
2117
|
+
type: "end",
|
|
2118
|
+
response: {
|
|
2119
|
+
message,
|
|
2120
|
+
usage,
|
|
2121
|
+
finishReason: mapFinishReason(finishReason)
|
|
2122
|
+
}
|
|
2123
|
+
};
|
|
2124
|
+
}
|
|
2125
|
+
async countTokens(_messages) {
|
|
2126
|
+
throw new Error("Mistral API does not currently support token counting");
|
|
2127
|
+
}
|
|
2128
|
+
buildChatRequest(messages, options) {
|
|
2129
|
+
const model = options.model ?? this.defaultModel;
|
|
2130
|
+
const mappedMessages = messages.map((message) => this.toMistralMessage(message));
|
|
2131
|
+
const request = {
|
|
2132
|
+
model,
|
|
2133
|
+
messages: mappedMessages
|
|
2134
|
+
};
|
|
2135
|
+
if (options.temperature != null) {
|
|
2136
|
+
request.temperature = options.temperature;
|
|
2137
|
+
}
|
|
2138
|
+
if (options.topP != null) {
|
|
2139
|
+
request.topP = options.topP;
|
|
2140
|
+
}
|
|
2141
|
+
if (options.maxOutputTokens != null) {
|
|
2142
|
+
request.maxTokens = options.maxOutputTokens;
|
|
2143
|
+
}
|
|
2144
|
+
if (options.stopSequences?.length) {
|
|
2145
|
+
request.stop = options.stopSequences.length === 1 ? options.stopSequences[0] : options.stopSequences;
|
|
2146
|
+
}
|
|
2147
|
+
if (options.tools?.length) {
|
|
2148
|
+
request.tools = options.tools.map((tool) => ({
|
|
2149
|
+
type: "function",
|
|
2150
|
+
function: {
|
|
2151
|
+
name: tool.name,
|
|
2152
|
+
description: tool.description,
|
|
2153
|
+
parameters: typeof tool.inputSchema === "object" && tool.inputSchema !== null ? tool.inputSchema : {}
|
|
2154
|
+
}
|
|
2155
|
+
}));
|
|
2156
|
+
}
|
|
2157
|
+
if (options.responseFormat === "json") {
|
|
2158
|
+
request.responseFormat = { type: "json_object" };
|
|
2159
|
+
}
|
|
2160
|
+
return request;
|
|
2161
|
+
}
|
|
2162
|
+
buildLLMResponse(response) {
|
|
2163
|
+
const firstChoice = response.choices[0];
|
|
2164
|
+
if (!firstChoice) {
|
|
2165
|
+
return {
|
|
2166
|
+
message: {
|
|
2167
|
+
role: "assistant",
|
|
2168
|
+
content: [{ type: "text", text: "" }]
|
|
2169
|
+
},
|
|
2170
|
+
usage: this.fromUsage(response.usage),
|
|
2171
|
+
raw: response
|
|
2172
|
+
};
|
|
2173
|
+
}
|
|
2174
|
+
const message = this.fromAssistantMessage(firstChoice.message);
|
|
2175
|
+
return {
|
|
2176
|
+
message,
|
|
2177
|
+
usage: this.fromUsage(response.usage),
|
|
2178
|
+
finishReason: mapFinishReason(firstChoice.finishReason),
|
|
2179
|
+
raw: response
|
|
2180
|
+
};
|
|
2181
|
+
}
|
|
2182
|
+
fromUsage(usage) {
|
|
2183
|
+
if (!usage)
|
|
2184
|
+
return;
|
|
2185
|
+
return {
|
|
2186
|
+
promptTokens: usage.promptTokens ?? 0,
|
|
2187
|
+
completionTokens: usage.completionTokens ?? 0,
|
|
2188
|
+
totalTokens: usage.totalTokens ?? 0
|
|
2189
|
+
};
|
|
2190
|
+
}
|
|
2191
|
+
fromAssistantMessage(message) {
|
|
2192
|
+
const parts = [];
|
|
2193
|
+
if (typeof message.content === "string") {
|
|
2194
|
+
parts.push({ type: "text", text: message.content });
|
|
2195
|
+
} else if (Array.isArray(message.content)) {
|
|
2196
|
+
message.content.forEach((chunk) => {
|
|
2197
|
+
if (chunk.type === "text" && "text" in chunk) {
|
|
2198
|
+
parts.push({ type: "text", text: chunk.text });
|
|
2199
|
+
}
|
|
2200
|
+
});
|
|
2201
|
+
}
|
|
2202
|
+
const toolCalls = message.toolCalls?.map((call, index) => this.fromMistralToolCall(call, index)) ?? [];
|
|
2203
|
+
if (toolCalls.length > 0) {
|
|
2204
|
+
parts.splice(0, 0, ...toolCalls);
|
|
2205
|
+
}
|
|
2206
|
+
if (parts.length === 0) {
|
|
2207
|
+
parts.push({ type: "text", text: "" });
|
|
2208
|
+
}
|
|
2209
|
+
return {
|
|
2210
|
+
role: "assistant",
|
|
2211
|
+
content: parts
|
|
2212
|
+
};
|
|
2213
|
+
}
|
|
2214
|
+
fromMistralToolCall(call, index) {
|
|
2215
|
+
const args = typeof call.function.arguments === "string" ? call.function.arguments : JSON.stringify(call.function.arguments);
|
|
2216
|
+
return {
|
|
2217
|
+
type: "tool-call",
|
|
2218
|
+
id: call.id ?? `tool-call-${index}`,
|
|
2219
|
+
name: call.function.name,
|
|
2220
|
+
arguments: args
|
|
2221
|
+
};
|
|
2222
|
+
}
|
|
2223
|
+
toMistralMessage(message) {
|
|
2224
|
+
const textContent = this.extractText(message.content);
|
|
2225
|
+
const toolCalls = this.extractToolCalls(message);
|
|
2226
|
+
switch (message.role) {
|
|
2227
|
+
case "system":
|
|
2228
|
+
return {
|
|
2229
|
+
role: "system",
|
|
2230
|
+
content: textContent ?? ""
|
|
2231
|
+
};
|
|
2232
|
+
case "user":
|
|
2233
|
+
return {
|
|
2234
|
+
role: "user",
|
|
2235
|
+
content: textContent ?? ""
|
|
2236
|
+
};
|
|
2237
|
+
case "assistant":
|
|
2238
|
+
return {
|
|
2239
|
+
role: "assistant",
|
|
2240
|
+
content: toolCalls.length > 0 ? null : textContent ?? "",
|
|
2241
|
+
toolCalls: toolCalls.length > 0 ? toolCalls : undefined
|
|
2242
|
+
};
|
|
2243
|
+
case "tool":
|
|
2244
|
+
return {
|
|
2245
|
+
role: "tool",
|
|
2246
|
+
content: textContent ?? "",
|
|
2247
|
+
toolCallId: message.toolCallId ?? toolCalls[0]?.id
|
|
2248
|
+
};
|
|
2249
|
+
default:
|
|
2250
|
+
return {
|
|
2251
|
+
role: "user",
|
|
2252
|
+
content: textContent ?? ""
|
|
2253
|
+
};
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
extractText(parts) {
|
|
2257
|
+
const textParts = parts.filter((part) => part.type === "text").map((part) => part.text);
|
|
2258
|
+
if (textParts.length === 0)
|
|
2259
|
+
return null;
|
|
2260
|
+
return textParts.join("");
|
|
2261
|
+
}
|
|
2262
|
+
extractToolCalls(message) {
|
|
2263
|
+
const toolCallParts = message.content.filter((part) => part.type === "tool-call");
|
|
2264
|
+
return toolCallParts.map((call, index) => ({
|
|
2265
|
+
id: call.id ?? `call_${index}`,
|
|
2266
|
+
type: "function",
|
|
2267
|
+
index,
|
|
2268
|
+
function: {
|
|
2269
|
+
name: call.name,
|
|
2270
|
+
arguments: call.arguments
|
|
2271
|
+
}
|
|
2272
|
+
}));
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
function mapFinishReason(reason) {
|
|
2276
|
+
if (!reason)
|
|
2277
|
+
return;
|
|
2278
|
+
const normalized = reason.toLowerCase();
|
|
2279
|
+
switch (normalized) {
|
|
2280
|
+
case "stop":
|
|
2281
|
+
return "stop";
|
|
2282
|
+
case "length":
|
|
2283
|
+
return "length";
|
|
2284
|
+
case "tool_call":
|
|
2285
|
+
case "tool_calls":
|
|
2286
|
+
return "tool_call";
|
|
2287
|
+
case "content_filter":
|
|
2288
|
+
return "content_filter";
|
|
2289
|
+
default:
|
|
2290
|
+
return;
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
// src/impls/mistral-embedding.ts
|
|
2295
|
+
import { Mistral as Mistral2 } from "@mistralai/mistralai";
|
|
2296
|
+
|
|
2297
|
+
class MistralEmbeddingProvider {
|
|
2298
|
+
client;
|
|
2299
|
+
defaultModel;
|
|
2300
|
+
constructor(options) {
|
|
2301
|
+
if (!options.apiKey) {
|
|
2302
|
+
throw new Error("MistralEmbeddingProvider requires an apiKey");
|
|
2303
|
+
}
|
|
2304
|
+
this.client = options.client ?? new Mistral2({
|
|
2305
|
+
apiKey: options.apiKey,
|
|
2306
|
+
serverURL: options.serverURL
|
|
2307
|
+
});
|
|
2308
|
+
this.defaultModel = options.defaultModel ?? "mistral-embed";
|
|
2309
|
+
}
|
|
2310
|
+
async embedDocuments(documents, options) {
|
|
2311
|
+
if (documents.length === 0)
|
|
2312
|
+
return [];
|
|
2313
|
+
const model = options?.model ?? this.defaultModel;
|
|
2314
|
+
const response = await this.client.embeddings.create({
|
|
2315
|
+
model,
|
|
2316
|
+
inputs: documents.map((doc) => doc.text)
|
|
2317
|
+
});
|
|
2318
|
+
return response.data.map((item, index) => ({
|
|
2319
|
+
id: documents[index]?.id ?? (item.index != null ? `embedding-${item.index}` : `embedding-${index}`),
|
|
2320
|
+
vector: item.embedding ?? [],
|
|
2321
|
+
dimensions: item.embedding?.length ?? 0,
|
|
2322
|
+
model: response.model,
|
|
2323
|
+
metadata: documents[index]?.metadata ? Object.fromEntries(Object.entries(documents[index]?.metadata ?? {}).map(([key, value]) => [key, String(value)])) : undefined
|
|
2324
|
+
}));
|
|
2325
|
+
}
|
|
2326
|
+
async embedQuery(query, options) {
|
|
2327
|
+
const [result] = await this.embedDocuments([{ id: "query", text: query }], options);
|
|
2328
|
+
if (!result) {
|
|
2329
|
+
throw new Error("Failed to compute embedding for query");
|
|
2330
|
+
}
|
|
2331
|
+
return result;
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
// src/impls/qdrant-vector.ts
|
|
2336
|
+
import { QdrantClient } from "@qdrant/js-client-rest";
|
|
2337
|
+
|
|
2338
|
+
class QdrantVectorProvider {
|
|
2339
|
+
client;
|
|
2340
|
+
createCollectionIfMissing;
|
|
2341
|
+
distance;
|
|
2342
|
+
constructor(options) {
|
|
2343
|
+
this.client = options.client ?? new QdrantClient({
|
|
2344
|
+
url: options.url,
|
|
2345
|
+
apiKey: options.apiKey,
|
|
2346
|
+
...options.clientParams
|
|
2347
|
+
});
|
|
2348
|
+
this.createCollectionIfMissing = options.createCollectionIfMissing ?? true;
|
|
2349
|
+
this.distance = options.distance ?? "Cosine";
|
|
2350
|
+
}
|
|
2351
|
+
async upsert(request) {
|
|
2352
|
+
if (request.documents.length === 0)
|
|
2353
|
+
return;
|
|
2354
|
+
const firstDocument = request.documents[0];
|
|
2355
|
+
if (!firstDocument)
|
|
2356
|
+
return;
|
|
2357
|
+
const vectorSize = firstDocument.vector.length;
|
|
2358
|
+
if (this.createCollectionIfMissing) {
|
|
2359
|
+
await this.ensureCollection(request.collection, vectorSize);
|
|
2360
|
+
}
|
|
2361
|
+
const points = request.documents.map((document) => ({
|
|
2362
|
+
id: document.id,
|
|
2363
|
+
vector: document.vector,
|
|
2364
|
+
payload: {
|
|
2365
|
+
...document.payload,
|
|
2366
|
+
...document.namespace ? { namespace: document.namespace } : {},
|
|
2367
|
+
...document.expiresAt ? { expiresAt: document.expiresAt.toISOString() } : {}
|
|
2368
|
+
}
|
|
2369
|
+
}));
|
|
2370
|
+
await this.client.upsert(request.collection, {
|
|
2371
|
+
wait: true,
|
|
2372
|
+
points
|
|
2373
|
+
});
|
|
2374
|
+
}
|
|
2375
|
+
async search(query) {
|
|
2376
|
+
const results = await this.client.search(query.collection, {
|
|
2377
|
+
vector: query.vector,
|
|
2378
|
+
limit: query.topK,
|
|
2379
|
+
filter: query.filter,
|
|
2380
|
+
score_threshold: query.scoreThreshold,
|
|
2381
|
+
with_payload: true,
|
|
2382
|
+
with_vector: false
|
|
2383
|
+
});
|
|
2384
|
+
return results.map((item) => ({
|
|
2385
|
+
id: String(item.id),
|
|
2386
|
+
score: item.score,
|
|
2387
|
+
payload: item.payload ?? undefined,
|
|
2388
|
+
namespace: typeof item.payload === "object" && item.payload !== null ? item.payload.namespace : undefined
|
|
2389
|
+
}));
|
|
2390
|
+
}
|
|
2391
|
+
async delete(request) {
|
|
2392
|
+
await this.client.delete(request.collection, {
|
|
2393
|
+
wait: true,
|
|
2394
|
+
points: request.ids
|
|
2395
|
+
});
|
|
2396
|
+
}
|
|
2397
|
+
async ensureCollection(collectionName, vectorSize) {
|
|
2398
|
+
try {
|
|
2399
|
+
await this.client.getCollection(collectionName);
|
|
2400
|
+
} catch (_error) {
|
|
2401
|
+
await this.client.createCollection(collectionName, {
|
|
2402
|
+
vectors: {
|
|
2403
|
+
size: vectorSize,
|
|
2404
|
+
distance: this.distance
|
|
2405
|
+
}
|
|
2406
|
+
});
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2411
|
+
// src/impls/supabase-psql.ts
|
|
2412
|
+
import { Buffer as Buffer3 } from "node:buffer";
|
|
2413
|
+
import { sql as drizzleSql } from "drizzle-orm";
|
|
2414
|
+
import { drizzle } from "drizzle-orm/postgres-js";
|
|
2415
|
+
import postgres from "postgres";
|
|
2416
|
+
|
|
2417
|
+
class SupabasePostgresProvider {
|
|
2418
|
+
client;
|
|
2419
|
+
db;
|
|
2420
|
+
ownsClient;
|
|
2421
|
+
createDrizzle;
|
|
2422
|
+
constructor(options = {}) {
|
|
2423
|
+
this.createDrizzle = options.createDrizzle ?? ((client) => drizzle(client));
|
|
2424
|
+
if (options.db) {
|
|
2425
|
+
if (!options.client) {
|
|
2426
|
+
throw new Error("SupabasePostgresProvider requires a postgres client when db is provided.");
|
|
2427
|
+
}
|
|
2428
|
+
this.client = options.client;
|
|
2429
|
+
this.db = options.db;
|
|
2430
|
+
this.ownsClient = false;
|
|
2431
|
+
return;
|
|
2432
|
+
}
|
|
2433
|
+
if (options.client) {
|
|
2434
|
+
this.client = options.client;
|
|
2435
|
+
this.ownsClient = false;
|
|
2436
|
+
} else {
|
|
2437
|
+
if (!options.connectionString) {
|
|
2438
|
+
throw new Error("SupabasePostgresProvider requires either a connectionString or a client.");
|
|
2439
|
+
}
|
|
2440
|
+
this.client = postgres(options.connectionString, {
|
|
2441
|
+
max: options.maxConnections,
|
|
2442
|
+
prepare: false,
|
|
2443
|
+
ssl: resolveSslMode(options.sslMode)
|
|
2444
|
+
});
|
|
2445
|
+
this.ownsClient = true;
|
|
2446
|
+
}
|
|
2447
|
+
this.db = this.createDrizzle(this.client);
|
|
2448
|
+
}
|
|
2449
|
+
async query(statement, params = []) {
|
|
2450
|
+
const query = buildParameterizedSql(statement, params);
|
|
2451
|
+
const result = await this.db.execute(query);
|
|
2452
|
+
const rows = asRows(result);
|
|
2453
|
+
return {
|
|
2454
|
+
rows,
|
|
2455
|
+
rowCount: rows.length
|
|
2456
|
+
};
|
|
2457
|
+
}
|
|
2458
|
+
async execute(statement, params = []) {
|
|
2459
|
+
const query = buildParameterizedSql(statement, params);
|
|
2460
|
+
await this.db.execute(query);
|
|
2461
|
+
}
|
|
2462
|
+
async transaction(run) {
|
|
2463
|
+
const transactionResult = this.client.begin(async (transactionClient) => {
|
|
2464
|
+
const transactionalProvider = new SupabasePostgresProvider({
|
|
2465
|
+
client: transactionClient,
|
|
2466
|
+
db: this.createDrizzle(transactionClient),
|
|
2467
|
+
createDrizzle: this.createDrizzle
|
|
2468
|
+
});
|
|
2469
|
+
return run(transactionalProvider);
|
|
2470
|
+
});
|
|
2471
|
+
return transactionResult;
|
|
2472
|
+
}
|
|
2473
|
+
async close() {
|
|
2474
|
+
if (this.ownsClient) {
|
|
2475
|
+
await this.client.end({ timeout: 5 });
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
function buildParameterizedSql(statement, params) {
|
|
2480
|
+
const segments = [];
|
|
2481
|
+
const pattern = /\$(\d+)/g;
|
|
2482
|
+
let cursor = 0;
|
|
2483
|
+
for (const match of statement.matchAll(pattern)) {
|
|
2484
|
+
const token = match[0];
|
|
2485
|
+
const indexPart = match[1];
|
|
2486
|
+
const start = match.index;
|
|
2487
|
+
if (indexPart == null || start == null)
|
|
2488
|
+
continue;
|
|
2489
|
+
const parameterIndex = Number(indexPart) - 1;
|
|
2490
|
+
if (!Number.isInteger(parameterIndex) || parameterIndex < 0 || parameterIndex >= params.length) {
|
|
2491
|
+
throw new Error(`SQL placeholder ${token} is out of bounds for ${params.length} parameter(s).`);
|
|
2492
|
+
}
|
|
2493
|
+
const staticSegment = statement.slice(cursor, start);
|
|
2494
|
+
if (staticSegment.length > 0) {
|
|
2495
|
+
segments.push(drizzleSql.raw(staticSegment));
|
|
2496
|
+
}
|
|
2497
|
+
const parameterValue = params[parameterIndex];
|
|
2498
|
+
if (parameterValue === undefined) {
|
|
2499
|
+
throw new Error(`SQL placeholder ${token} is missing a parameter value.`);
|
|
2500
|
+
}
|
|
2501
|
+
const normalizedValue = normalizeParam(parameterValue);
|
|
2502
|
+
segments.push(drizzleSql`${normalizedValue}`);
|
|
2503
|
+
cursor = start + token.length;
|
|
2504
|
+
}
|
|
2505
|
+
const tailSegment = statement.slice(cursor);
|
|
2506
|
+
if (tailSegment.length > 0) {
|
|
2507
|
+
segments.push(drizzleSql.raw(tailSegment));
|
|
2508
|
+
}
|
|
2509
|
+
if (segments.length === 0) {
|
|
2510
|
+
return drizzleSql.raw("");
|
|
2511
|
+
}
|
|
2512
|
+
return drizzleSql.join(segments);
|
|
2513
|
+
}
|
|
2514
|
+
function normalizeParam(value) {
|
|
2515
|
+
if (typeof value === "bigint") {
|
|
2516
|
+
return value.toString();
|
|
2517
|
+
}
|
|
2518
|
+
if (value instanceof Uint8Array) {
|
|
2519
|
+
return Buffer3.from(value);
|
|
2520
|
+
}
|
|
2521
|
+
if (isPlainObject(value)) {
|
|
2522
|
+
return JSON.stringify(value);
|
|
2523
|
+
}
|
|
2524
|
+
return value;
|
|
2525
|
+
}
|
|
2526
|
+
function asRows(result) {
|
|
2527
|
+
if (!Array.isArray(result)) {
|
|
2528
|
+
return [];
|
|
2529
|
+
}
|
|
2530
|
+
return result;
|
|
2531
|
+
}
|
|
2532
|
+
function isPlainObject(value) {
|
|
2533
|
+
if (value == null || typeof value !== "object") {
|
|
2534
|
+
return false;
|
|
2535
|
+
}
|
|
2536
|
+
if (Array.isArray(value)) {
|
|
2537
|
+
return false;
|
|
2538
|
+
}
|
|
2539
|
+
if (value instanceof Date) {
|
|
2540
|
+
return false;
|
|
2541
|
+
}
|
|
2542
|
+
if (value instanceof Uint8Array) {
|
|
2543
|
+
return false;
|
|
2544
|
+
}
|
|
2545
|
+
return true;
|
|
2546
|
+
}
|
|
2547
|
+
function resolveSslMode(mode) {
|
|
2548
|
+
switch (mode) {
|
|
2549
|
+
case "allow":
|
|
2550
|
+
return false;
|
|
2551
|
+
case "prefer":
|
|
2552
|
+
return "prefer";
|
|
2553
|
+
case "require":
|
|
2554
|
+
default:
|
|
2555
|
+
return "require";
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
|
|
2559
|
+
// src/impls/supabase-vector.ts
|
|
2560
|
+
class SupabaseVectorProvider {
|
|
2561
|
+
database;
|
|
2562
|
+
createTableIfMissing;
|
|
2563
|
+
distanceMetric;
|
|
2564
|
+
quotedSchema;
|
|
2565
|
+
qualifiedTable;
|
|
2566
|
+
collectionIndex;
|
|
2567
|
+
namespaceIndex;
|
|
2568
|
+
ensureTablePromise;
|
|
2569
|
+
constructor(options) {
|
|
2570
|
+
this.database = options.database ?? new SupabasePostgresProvider({
|
|
2571
|
+
connectionString: options.connectionString,
|
|
2572
|
+
maxConnections: options.maxConnections,
|
|
2573
|
+
sslMode: options.sslMode
|
|
2574
|
+
});
|
|
2575
|
+
this.createTableIfMissing = options.createTableIfMissing ?? true;
|
|
2576
|
+
this.distanceMetric = options.distanceMetric ?? "cosine";
|
|
2577
|
+
const schema = sanitizeIdentifier(options.schema ?? "public", "schema");
|
|
2578
|
+
const table = sanitizeIdentifier(options.table ?? "contractspec_vectors", "table");
|
|
2579
|
+
this.quotedSchema = quoteIdentifier(schema);
|
|
2580
|
+
this.qualifiedTable = `${this.quotedSchema}.${quoteIdentifier(table)}`;
|
|
2581
|
+
this.collectionIndex = quoteIdentifier(`${table}_collection_idx`);
|
|
2582
|
+
this.namespaceIndex = quoteIdentifier(`${table}_namespace_idx`);
|
|
2583
|
+
}
|
|
2584
|
+
async upsert(request) {
|
|
2585
|
+
if (request.documents.length === 0) {
|
|
2586
|
+
return;
|
|
2587
|
+
}
|
|
2588
|
+
if (this.createTableIfMissing) {
|
|
2589
|
+
await this.ensureTable();
|
|
2590
|
+
}
|
|
2591
|
+
for (const document of request.documents) {
|
|
2592
|
+
await this.database.execute(`INSERT INTO ${this.qualifiedTable}
|
|
2593
|
+
(collection, id, embedding, payload, namespace, expires_at, updated_at)
|
|
2594
|
+
VALUES ($1, $2, $3::vector, $4::jsonb, $5, $6, now())
|
|
2595
|
+
ON CONFLICT (collection, id)
|
|
2596
|
+
DO UPDATE SET
|
|
2597
|
+
embedding = EXCLUDED.embedding,
|
|
2598
|
+
payload = EXCLUDED.payload,
|
|
2599
|
+
namespace = EXCLUDED.namespace,
|
|
2600
|
+
expires_at = EXCLUDED.expires_at,
|
|
2601
|
+
updated_at = now();`, [
|
|
2602
|
+
request.collection,
|
|
2603
|
+
document.id,
|
|
2604
|
+
toVectorLiteral(document.vector),
|
|
2605
|
+
document.payload ? JSON.stringify(document.payload) : null,
|
|
2606
|
+
document.namespace ?? null,
|
|
2607
|
+
document.expiresAt ?? null
|
|
2608
|
+
]);
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
async search(query) {
|
|
2612
|
+
const operator = this.distanceOperator;
|
|
2613
|
+
const results = await this.database.query(`SELECT
|
|
2614
|
+
id,
|
|
2615
|
+
payload,
|
|
2616
|
+
namespace,
|
|
2617
|
+
(embedding ${operator} $3::vector) AS distance
|
|
2618
|
+
FROM ${this.qualifiedTable}
|
|
2619
|
+
WHERE collection = $1
|
|
2620
|
+
AND ($2::text IS NULL OR namespace = $2)
|
|
2621
|
+
AND (expires_at IS NULL OR expires_at > now())
|
|
2622
|
+
AND ($4::jsonb IS NULL OR payload @> $4::jsonb)
|
|
2623
|
+
ORDER BY embedding ${operator} $3::vector
|
|
2624
|
+
LIMIT $5;`, [
|
|
2625
|
+
query.collection,
|
|
2626
|
+
query.namespace ?? null,
|
|
2627
|
+
toVectorLiteral(query.vector),
|
|
2628
|
+
query.filter ? JSON.stringify(query.filter) : null,
|
|
2629
|
+
query.topK
|
|
2630
|
+
]);
|
|
2631
|
+
const mapped = results.rows.map((row) => {
|
|
2632
|
+
const distance = Number(row.distance);
|
|
2633
|
+
return {
|
|
2634
|
+
id: row.id,
|
|
2635
|
+
score: distanceToScore(distance, this.distanceMetric),
|
|
2636
|
+
payload: isRecord(row.payload) ? row.payload : undefined,
|
|
2637
|
+
namespace: row.namespace ?? undefined
|
|
2638
|
+
};
|
|
2639
|
+
});
|
|
2640
|
+
const scoreThreshold = query.scoreThreshold;
|
|
2641
|
+
if (scoreThreshold == null) {
|
|
2642
|
+
return mapped;
|
|
2643
|
+
}
|
|
2644
|
+
return mapped.filter((result) => result.score >= scoreThreshold);
|
|
2645
|
+
}
|
|
2646
|
+
async delete(request) {
|
|
2647
|
+
if (request.ids.length === 0) {
|
|
2648
|
+
return;
|
|
2649
|
+
}
|
|
2650
|
+
const params = [
|
|
2651
|
+
request.collection,
|
|
2652
|
+
request.ids,
|
|
2653
|
+
request.namespace ?? null
|
|
2654
|
+
];
|
|
2655
|
+
await this.database.execute(`DELETE FROM ${this.qualifiedTable}
|
|
2656
|
+
WHERE collection = $1
|
|
2657
|
+
AND id = ANY($2::text[])
|
|
2658
|
+
AND ($3::text IS NULL OR namespace = $3);`, params);
|
|
2659
|
+
}
|
|
2660
|
+
async ensureTable() {
|
|
2661
|
+
if (!this.ensureTablePromise) {
|
|
2662
|
+
this.ensureTablePromise = this.createTable();
|
|
2663
|
+
}
|
|
2664
|
+
await this.ensureTablePromise;
|
|
2665
|
+
}
|
|
2666
|
+
async createTable() {
|
|
2667
|
+
await this.database.execute("CREATE EXTENSION IF NOT EXISTS vector;");
|
|
2668
|
+
await this.database.execute(`CREATE SCHEMA IF NOT EXISTS ${this.quotedSchema};`);
|
|
2669
|
+
await this.database.execute(`CREATE TABLE IF NOT EXISTS ${this.qualifiedTable} (
|
|
2670
|
+
collection text NOT NULL,
|
|
2671
|
+
id text NOT NULL,
|
|
2672
|
+
embedding vector NOT NULL,
|
|
2673
|
+
payload jsonb,
|
|
2674
|
+
namespace text,
|
|
2675
|
+
expires_at timestamptz,
|
|
2676
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
2677
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
2678
|
+
PRIMARY KEY (collection, id)
|
|
2679
|
+
);`);
|
|
2680
|
+
await this.database.execute(`CREATE INDEX IF NOT EXISTS ${this.collectionIndex}
|
|
2681
|
+
ON ${this.qualifiedTable} (collection);`);
|
|
2682
|
+
await this.database.execute(`CREATE INDEX IF NOT EXISTS ${this.namespaceIndex}
|
|
2683
|
+
ON ${this.qualifiedTable} (namespace);`);
|
|
2684
|
+
}
|
|
2685
|
+
get distanceOperator() {
|
|
2686
|
+
switch (this.distanceMetric) {
|
|
2687
|
+
case "l2":
|
|
2688
|
+
return "<->";
|
|
2689
|
+
case "inner_product":
|
|
2690
|
+
return "<#>";
|
|
2691
|
+
case "cosine":
|
|
2692
|
+
default:
|
|
2693
|
+
return "<=>";
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
function sanitizeIdentifier(value, label) {
|
|
2698
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)) {
|
|
2699
|
+
throw new Error(`SupabaseVectorProvider ${label} "${value}" is invalid.`);
|
|
2700
|
+
}
|
|
2701
|
+
return value;
|
|
2702
|
+
}
|
|
2703
|
+
function quoteIdentifier(value) {
|
|
2704
|
+
return `"${value.replaceAll('"', '""')}"`;
|
|
2705
|
+
}
|
|
2706
|
+
function toVectorLiteral(vector) {
|
|
2707
|
+
if (vector.length === 0) {
|
|
2708
|
+
throw new Error("Supabase vectors must contain at least one dimension.");
|
|
2709
|
+
}
|
|
2710
|
+
for (const value of vector) {
|
|
2711
|
+
if (!Number.isFinite(value)) {
|
|
2712
|
+
throw new Error(`Supabase vectors must be finite numbers. Found "${value}".`);
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
return `[${vector.join(",")}]`;
|
|
2716
|
+
}
|
|
2717
|
+
function isRecord(value) {
|
|
2718
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2719
|
+
}
|
|
2720
|
+
function distanceToScore(distance, metric) {
|
|
2721
|
+
switch (metric) {
|
|
2722
|
+
case "inner_product":
|
|
2723
|
+
return -distance;
|
|
2724
|
+
case "l2":
|
|
2725
|
+
return 1 / (1 + distance);
|
|
2726
|
+
case "cosine":
|
|
2727
|
+
default:
|
|
2728
|
+
return 1 - distance;
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2732
|
+
// src/impls/stripe-payments.ts
|
|
2733
|
+
import Stripe from "stripe";
|
|
2734
|
+
var API_VERSION = "2026-01-28.clover";
|
|
2735
|
+
|
|
2736
|
+
class StripePaymentsProvider {
|
|
2737
|
+
stripe;
|
|
2738
|
+
constructor(options) {
|
|
2739
|
+
this.stripe = options.stripe ?? new Stripe(options.apiKey, {
|
|
2740
|
+
apiVersion: API_VERSION
|
|
2741
|
+
});
|
|
2742
|
+
}
|
|
2743
|
+
async createCustomer(input) {
|
|
2744
|
+
const customer = await this.stripe.customers.create({
|
|
2745
|
+
email: input.email,
|
|
2746
|
+
name: input.name,
|
|
2747
|
+
description: input.description,
|
|
2748
|
+
metadata: input.metadata
|
|
2749
|
+
});
|
|
2750
|
+
return this.toCustomer(customer);
|
|
2751
|
+
}
|
|
2752
|
+
async getCustomer(customerId) {
|
|
2753
|
+
const customer = await this.stripe.customers.retrieve(customerId);
|
|
2754
|
+
if (customer.deleted)
|
|
2755
|
+
return null;
|
|
2756
|
+
return this.toCustomer(customer);
|
|
2757
|
+
}
|
|
2758
|
+
async createPaymentIntent(input) {
|
|
2759
|
+
const intent = await this.stripe.paymentIntents.create({
|
|
2760
|
+
amount: input.amount.amount,
|
|
2761
|
+
currency: input.amount.currency,
|
|
2762
|
+
customer: input.customerId,
|
|
2763
|
+
description: input.description,
|
|
2764
|
+
capture_method: input.captureMethod ?? "automatic",
|
|
2765
|
+
confirmation_method: input.confirmationMethod ?? "automatic",
|
|
2766
|
+
automatic_payment_methods: { enabled: true },
|
|
2767
|
+
metadata: input.metadata,
|
|
2768
|
+
return_url: input.returnUrl,
|
|
2769
|
+
statement_descriptor: input.statementDescriptor
|
|
2770
|
+
});
|
|
2771
|
+
return this.toPaymentIntent(intent);
|
|
2772
|
+
}
|
|
2773
|
+
async capturePayment(paymentIntentId, input) {
|
|
2774
|
+
const intent = await this.stripe.paymentIntents.capture(paymentIntentId, input?.amount ? { amount_to_capture: input.amount.amount } : undefined);
|
|
2775
|
+
return this.toPaymentIntent(intent);
|
|
2776
|
+
}
|
|
2777
|
+
async cancelPaymentIntent(paymentIntentId) {
|
|
2778
|
+
const intent = await this.stripe.paymentIntents.cancel(paymentIntentId);
|
|
2779
|
+
return this.toPaymentIntent(intent);
|
|
2780
|
+
}
|
|
2781
|
+
async refundPayment(input) {
|
|
2782
|
+
const refund = await this.stripe.refunds.create({
|
|
2783
|
+
payment_intent: input.paymentIntentId,
|
|
2784
|
+
amount: input.amount?.amount,
|
|
2785
|
+
reason: mapRefundReason(input.reason),
|
|
2786
|
+
metadata: input.metadata
|
|
2787
|
+
});
|
|
2788
|
+
const paymentIntentId = typeof refund.payment_intent === "string" ? refund.payment_intent : refund.payment_intent?.id ?? "";
|
|
2789
|
+
return {
|
|
2790
|
+
id: refund.id,
|
|
2791
|
+
paymentIntentId,
|
|
2792
|
+
amount: {
|
|
2793
|
+
amount: refund.amount ?? 0,
|
|
2794
|
+
currency: refund.currency?.toUpperCase() ?? "USD"
|
|
2795
|
+
},
|
|
2796
|
+
status: mapRefundStatus(refund.status),
|
|
2797
|
+
reason: refund.reason ?? undefined,
|
|
2798
|
+
metadata: this.toMetadata(refund.metadata),
|
|
2799
|
+
createdAt: refund.created ? new Date(refund.created * 1000) : undefined
|
|
2800
|
+
};
|
|
2801
|
+
}
|
|
2802
|
+
async listInvoices(query) {
|
|
2803
|
+
const requestedStatus = query?.status?.[0];
|
|
2804
|
+
const stripeStatus = requestedStatus && requestedStatus !== "deleted" ? requestedStatus : undefined;
|
|
2805
|
+
const response = await this.stripe.invoices.list({
|
|
2806
|
+
customer: query?.customerId,
|
|
2807
|
+
status: stripeStatus,
|
|
2808
|
+
limit: query?.limit,
|
|
2809
|
+
starting_after: query?.startingAfter
|
|
2810
|
+
});
|
|
2811
|
+
return response.data.map((invoice) => this.toInvoice(invoice));
|
|
2812
|
+
}
|
|
2813
|
+
async listTransactions(query) {
|
|
2814
|
+
const response = await this.stripe.charges.list({
|
|
2815
|
+
customer: query?.customerId,
|
|
2816
|
+
payment_intent: query?.paymentIntentId,
|
|
2817
|
+
limit: query?.limit,
|
|
2818
|
+
starting_after: query?.startingAfter
|
|
2819
|
+
});
|
|
2820
|
+
return response.data.map((charge) => ({
|
|
2821
|
+
id: charge.id,
|
|
2822
|
+
paymentIntentId: typeof charge.payment_intent === "string" ? charge.payment_intent : charge.payment_intent?.id,
|
|
2823
|
+
amount: {
|
|
2824
|
+
amount: charge.amount,
|
|
2825
|
+
currency: charge.currency?.toUpperCase() ?? "USD"
|
|
2826
|
+
},
|
|
2827
|
+
type: "capture",
|
|
2828
|
+
status: mapChargeStatus(charge.status),
|
|
2829
|
+
description: charge.description ?? undefined,
|
|
2830
|
+
createdAt: new Date(charge.created * 1000),
|
|
2831
|
+
metadata: this.mergeMetadata(this.toMetadata(charge.metadata), {
|
|
2832
|
+
balanceTransaction: typeof charge.balance_transaction === "string" ? charge.balance_transaction : undefined
|
|
2833
|
+
})
|
|
2834
|
+
}));
|
|
2835
|
+
}
|
|
2836
|
+
toCustomer(customer) {
|
|
2837
|
+
const metadata = this.toMetadata(customer.metadata);
|
|
2838
|
+
const updatedAtValue = metadata?.updatedAt;
|
|
2839
|
+
return {
|
|
2840
|
+
id: customer.id,
|
|
2841
|
+
email: customer.email ?? undefined,
|
|
2842
|
+
name: customer.name ?? undefined,
|
|
2843
|
+
metadata,
|
|
2844
|
+
createdAt: customer.created ? new Date(customer.created * 1000) : undefined,
|
|
2845
|
+
updatedAt: updatedAtValue ? new Date(updatedAtValue) : undefined
|
|
2846
|
+
};
|
|
2847
|
+
}
|
|
2848
|
+
toPaymentIntent(intent) {
|
|
2849
|
+
const metadata = this.toMetadata(intent.metadata);
|
|
2850
|
+
return {
|
|
2851
|
+
id: intent.id,
|
|
2852
|
+
amount: this.toMoney(intent.amount_received ?? intent.amount ?? 0, intent.currency),
|
|
2853
|
+
status: mapPaymentIntentStatus(intent.status),
|
|
2854
|
+
customerId: typeof intent.customer === "string" ? intent.customer : intent.customer?.id,
|
|
2855
|
+
description: intent.description ?? undefined,
|
|
2856
|
+
clientSecret: intent.client_secret ?? undefined,
|
|
2857
|
+
metadata,
|
|
2858
|
+
createdAt: new Date(intent.created * 1000),
|
|
2859
|
+
updatedAt: intent.canceled_at != null ? new Date(intent.canceled_at * 1000) : new Date(intent.created * 1000)
|
|
2860
|
+
};
|
|
2861
|
+
}
|
|
2862
|
+
toInvoice(invoice) {
|
|
2863
|
+
const metadata = this.toMetadata(invoice.metadata);
|
|
2864
|
+
return {
|
|
2865
|
+
id: invoice.id,
|
|
2866
|
+
number: invoice.number ?? undefined,
|
|
2867
|
+
status: invoice.status ?? "draft",
|
|
2868
|
+
amountDue: this.toMoney(invoice.amount_due ?? 0, invoice.currency),
|
|
2869
|
+
amountPaid: this.toMoney(invoice.amount_paid ?? 0, invoice.currency),
|
|
2870
|
+
customerId: typeof invoice.customer === "string" ? invoice.customer : invoice.customer?.id,
|
|
2871
|
+
dueDate: invoice.due_date ? new Date(invoice.due_date * 1000) : undefined,
|
|
2872
|
+
hostedInvoiceUrl: invoice.hosted_invoice_url ?? undefined,
|
|
2873
|
+
metadata,
|
|
2874
|
+
createdAt: invoice.created ? new Date(invoice.created * 1000) : undefined,
|
|
2875
|
+
updatedAt: invoice.status_transitions?.finalized_at ? new Date(invoice.status_transitions.finalized_at * 1000) : undefined
|
|
2876
|
+
};
|
|
2877
|
+
}
|
|
2878
|
+
toMoney(amount, currency) {
|
|
2879
|
+
return {
|
|
2880
|
+
amount,
|
|
2881
|
+
currency: currency?.toUpperCase() ?? "USD"
|
|
2882
|
+
};
|
|
2883
|
+
}
|
|
2884
|
+
toMetadata(metadata) {
|
|
2885
|
+
if (!metadata)
|
|
2886
|
+
return;
|
|
2887
|
+
const entries = Object.entries(metadata).filter((entry) => typeof entry[1] === "string");
|
|
2888
|
+
if (entries.length === 0)
|
|
2889
|
+
return;
|
|
2890
|
+
return Object.fromEntries(entries);
|
|
2891
|
+
}
|
|
2892
|
+
mergeMetadata(base, extras) {
|
|
2893
|
+
const filteredExtras = Object.entries(extras).filter((entry) => typeof entry[1] === "string");
|
|
2894
|
+
if (!base && filteredExtras.length === 0) {
|
|
2895
|
+
return;
|
|
2896
|
+
}
|
|
2897
|
+
return {
|
|
2898
|
+
...base ?? {},
|
|
2899
|
+
...Object.fromEntries(filteredExtras)
|
|
2900
|
+
};
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
2903
|
+
function mapRefundReason(reason) {
|
|
2904
|
+
if (!reason)
|
|
2905
|
+
return;
|
|
2906
|
+
const allowed = [
|
|
2907
|
+
"duplicate",
|
|
2908
|
+
"fraudulent",
|
|
2909
|
+
"requested_by_customer"
|
|
2910
|
+
];
|
|
2911
|
+
return allowed.includes(reason) ? reason : undefined;
|
|
2912
|
+
}
|
|
2913
|
+
function mapPaymentIntentStatus(status) {
|
|
2914
|
+
switch (status) {
|
|
2915
|
+
case "requires_payment_method":
|
|
2916
|
+
return "requires_payment_method";
|
|
2917
|
+
case "requires_confirmation":
|
|
2918
|
+
return "requires_confirmation";
|
|
2919
|
+
case "requires_action":
|
|
2920
|
+
case "requires_capture":
|
|
2921
|
+
return "requires_action";
|
|
2922
|
+
case "processing":
|
|
2923
|
+
return "processing";
|
|
2924
|
+
case "succeeded":
|
|
2925
|
+
return "succeeded";
|
|
2926
|
+
case "canceled":
|
|
2927
|
+
return "canceled";
|
|
2928
|
+
default:
|
|
2929
|
+
return "requires_payment_method";
|
|
2930
|
+
}
|
|
2931
|
+
}
|
|
2932
|
+
function mapRefundStatus(status) {
|
|
2933
|
+
switch (status) {
|
|
2934
|
+
case "pending":
|
|
2935
|
+
case "succeeded":
|
|
2936
|
+
case "failed":
|
|
2937
|
+
case "canceled":
|
|
2938
|
+
return status;
|
|
2939
|
+
default:
|
|
2940
|
+
return "pending";
|
|
2941
|
+
}
|
|
2942
|
+
}
|
|
2943
|
+
function mapChargeStatus(status) {
|
|
2944
|
+
switch (status) {
|
|
2945
|
+
case "pending":
|
|
2946
|
+
case "processing":
|
|
2947
|
+
return "pending";
|
|
2948
|
+
case "succeeded":
|
|
2949
|
+
return "succeeded";
|
|
2950
|
+
case "failed":
|
|
2951
|
+
case "canceled":
|
|
2952
|
+
return "failed";
|
|
2953
|
+
default:
|
|
2954
|
+
return "pending";
|
|
2955
|
+
}
|
|
2956
|
+
}
|
|
2957
|
+
|
|
2958
|
+
// src/impls/postmark-email.ts
|
|
2959
|
+
import { ServerClient } from "postmark";
|
|
2960
|
+
|
|
2961
|
+
class PostmarkEmailProvider {
|
|
2962
|
+
client;
|
|
2963
|
+
defaultFromEmail;
|
|
2964
|
+
messageStream;
|
|
2965
|
+
constructor(options) {
|
|
2966
|
+
this.client = options.client ?? new ServerClient(options.serverToken, {
|
|
2967
|
+
useHttps: true
|
|
2968
|
+
});
|
|
2969
|
+
this.defaultFromEmail = options.defaultFromEmail;
|
|
2970
|
+
this.messageStream = options.messageStream;
|
|
2971
|
+
}
|
|
2972
|
+
async sendEmail(message) {
|
|
2973
|
+
const request = {
|
|
2974
|
+
From: formatAddress2(message.from) ?? this.defaultFromEmail,
|
|
2975
|
+
To: message.to.map((addr) => formatAddress2(addr)).join(", "),
|
|
2976
|
+
Cc: message.cc?.map((addr) => formatAddress2(addr)).join(", ") || undefined,
|
|
2977
|
+
Bcc: message.bcc?.map((addr) => formatAddress2(addr)).join(", ") || undefined,
|
|
2978
|
+
ReplyTo: message.replyTo ? formatAddress2(message.replyTo) : undefined,
|
|
2979
|
+
Subject: message.subject,
|
|
2980
|
+
TextBody: message.textBody,
|
|
2981
|
+
HtmlBody: message.htmlBody,
|
|
2982
|
+
Headers: message.headers ? Object.entries(message.headers).map(([name, value]) => ({
|
|
2983
|
+
Name: name,
|
|
2984
|
+
Value: value
|
|
2985
|
+
})) : undefined,
|
|
2986
|
+
MessageStream: this.messageStream,
|
|
2987
|
+
Attachments: buildAttachments(message)
|
|
2988
|
+
};
|
|
2989
|
+
const response = await this.client.sendEmail(request);
|
|
2990
|
+
return {
|
|
2991
|
+
id: response.MessageID,
|
|
2992
|
+
providerMessageId: response.MessageID,
|
|
2993
|
+
queuedAt: new Date(response.SubmittedAt ?? new Date().toISOString())
|
|
2994
|
+
};
|
|
2995
|
+
}
|
|
2996
|
+
}
|
|
2997
|
+
function formatAddress2(address) {
|
|
2998
|
+
if (address.name) {
|
|
2999
|
+
return `"${address.name}" <${address.email}>`;
|
|
3000
|
+
}
|
|
3001
|
+
return address.email;
|
|
3002
|
+
}
|
|
3003
|
+
function buildAttachments(message) {
|
|
3004
|
+
if (!message.attachments?.length)
|
|
3005
|
+
return;
|
|
3006
|
+
return message.attachments.filter((attachment) => attachment.data).map((attachment) => ({
|
|
3007
|
+
Name: attachment.filename,
|
|
3008
|
+
Content: Buffer.from(attachment.data ?? new Uint8Array).toString("base64"),
|
|
3009
|
+
ContentType: attachment.contentType,
|
|
3010
|
+
ContentID: null,
|
|
3011
|
+
ContentLength: attachment.sizeBytes,
|
|
3012
|
+
Disposition: "attachment"
|
|
3013
|
+
}));
|
|
3014
|
+
}
|
|
3015
|
+
|
|
3016
|
+
// src/impls/posthog-reader.ts
|
|
3017
|
+
class PosthogAnalyticsReader {
|
|
3018
|
+
projectId;
|
|
3019
|
+
client;
|
|
3020
|
+
constructor(options) {
|
|
3021
|
+
this.projectId = options.projectId;
|
|
3022
|
+
this.client = options.client;
|
|
3023
|
+
}
|
|
3024
|
+
async queryHogQL(input) {
|
|
3025
|
+
const projectId = resolveProjectId(undefined, this.projectId);
|
|
3026
|
+
const response = await this.client.request({
|
|
3027
|
+
method: "POST",
|
|
3028
|
+
path: `/api/projects/${projectId}/query`,
|
|
3029
|
+
body: {
|
|
3030
|
+
query: {
|
|
3031
|
+
kind: "HogQLQuery",
|
|
3032
|
+
query: input.query,
|
|
3033
|
+
values: input.values
|
|
3034
|
+
}
|
|
3035
|
+
}
|
|
3036
|
+
});
|
|
3037
|
+
return response.data;
|
|
3038
|
+
}
|
|
3039
|
+
async getEvents(input) {
|
|
3040
|
+
const projectId = resolveProjectId(input.projectId, this.projectId);
|
|
3041
|
+
const response = await this.client.request({
|
|
3042
|
+
method: "GET",
|
|
3043
|
+
path: `/api/projects/${projectId}/events/`,
|
|
3044
|
+
query: {
|
|
3045
|
+
event: input.event ?? resolveSingleEvent(input.events),
|
|
3046
|
+
events: resolveEventList(input.events),
|
|
3047
|
+
distinct_id: input.distinctId,
|
|
3048
|
+
order_by: input.orderBy,
|
|
3049
|
+
limit: input.limit,
|
|
3050
|
+
offset: input.offset,
|
|
3051
|
+
properties: input.properties ? JSON.stringify(input.properties) : undefined,
|
|
3052
|
+
...buildEventDateQuery(input.dateRange)
|
|
3053
|
+
}
|
|
3054
|
+
});
|
|
3055
|
+
return response.data;
|
|
3056
|
+
}
|
|
3057
|
+
async getPersons(input) {
|
|
3058
|
+
const projectId = resolveProjectId(input.projectId, this.projectId);
|
|
3059
|
+
const response = await this.client.request({
|
|
3060
|
+
method: "GET",
|
|
3061
|
+
path: `/api/projects/${projectId}/persons/`,
|
|
3062
|
+
query: {
|
|
3063
|
+
cohort_id: input.cohortId,
|
|
3064
|
+
search: input.search,
|
|
3065
|
+
limit: input.limit,
|
|
3066
|
+
offset: input.offset,
|
|
3067
|
+
properties: input.properties ? JSON.stringify(input.properties) : undefined
|
|
3068
|
+
}
|
|
3069
|
+
});
|
|
3070
|
+
return response.data;
|
|
3071
|
+
}
|
|
3072
|
+
async getInsights(input) {
|
|
3073
|
+
const projectId = resolveProjectId(input.projectId, this.projectId);
|
|
3074
|
+
const response = await this.client.request({
|
|
3075
|
+
method: "GET",
|
|
3076
|
+
path: `/api/projects/${projectId}/insights/`,
|
|
3077
|
+
query: {
|
|
3078
|
+
insight: input.insightType,
|
|
3079
|
+
limit: input.limit,
|
|
3080
|
+
offset: input.offset
|
|
3081
|
+
}
|
|
3082
|
+
});
|
|
3083
|
+
return response.data;
|
|
3084
|
+
}
|
|
3085
|
+
async getInsightResult(input) {
|
|
3086
|
+
const projectId = resolveProjectId(input.projectId, this.projectId);
|
|
3087
|
+
const response = await this.client.request({
|
|
3088
|
+
method: "GET",
|
|
3089
|
+
path: `/api/projects/${projectId}/insights/${input.insightId}/`
|
|
3090
|
+
});
|
|
3091
|
+
return response.data;
|
|
3092
|
+
}
|
|
3093
|
+
async getCohorts(input) {
|
|
3094
|
+
const projectId = resolveProjectId(input.projectId, this.projectId);
|
|
3095
|
+
const response = await this.client.request({
|
|
3096
|
+
method: "GET",
|
|
3097
|
+
path: `/api/projects/${projectId}/cohorts/`,
|
|
3098
|
+
query: {
|
|
3099
|
+
limit: input.limit,
|
|
3100
|
+
offset: input.offset
|
|
3101
|
+
}
|
|
3102
|
+
});
|
|
3103
|
+
return response.data;
|
|
3104
|
+
}
|
|
3105
|
+
async getFeatureFlags(input) {
|
|
3106
|
+
const projectId = resolveProjectId(input.projectId, this.projectId);
|
|
3107
|
+
const response = await this.client.request({
|
|
3108
|
+
method: "GET",
|
|
3109
|
+
path: `/api/projects/${projectId}/feature_flags/`,
|
|
3110
|
+
query: {
|
|
3111
|
+
active: input.active,
|
|
3112
|
+
limit: input.limit,
|
|
3113
|
+
offset: input.offset
|
|
3114
|
+
}
|
|
3115
|
+
});
|
|
3116
|
+
return response.data;
|
|
3117
|
+
}
|
|
3118
|
+
async getAnnotations(input) {
|
|
3119
|
+
const projectId = resolveProjectId(input.projectId, this.projectId);
|
|
3120
|
+
const response = await this.client.request({
|
|
3121
|
+
method: "GET",
|
|
3122
|
+
path: `/api/projects/${projectId}/annotations/`,
|
|
3123
|
+
query: {
|
|
3124
|
+
limit: input.limit,
|
|
3125
|
+
offset: input.offset,
|
|
3126
|
+
...buildAnnotationDateQuery(input.dateRange)
|
|
3127
|
+
}
|
|
3128
|
+
});
|
|
3129
|
+
return response.data;
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3132
|
+
function resolveProjectId(inputProjectId, defaultProjectId) {
|
|
3133
|
+
const projectId = inputProjectId ?? defaultProjectId;
|
|
3134
|
+
if (!projectId) {
|
|
3135
|
+
throw new Error("PostHog projectId is required for API reads.");
|
|
3136
|
+
}
|
|
3137
|
+
return projectId;
|
|
3138
|
+
}
|
|
3139
|
+
function resolveSingleEvent(events) {
|
|
3140
|
+
if (!events || events.length !== 1)
|
|
3141
|
+
return;
|
|
3142
|
+
return events[0];
|
|
3143
|
+
}
|
|
3144
|
+
function resolveEventList(events) {
|
|
3145
|
+
if (!events || events.length <= 1)
|
|
3146
|
+
return;
|
|
3147
|
+
return events.join(",");
|
|
3148
|
+
}
|
|
3149
|
+
function buildEventDateQuery(range) {
|
|
3150
|
+
const after = formatDate(range?.from);
|
|
3151
|
+
const before = formatDate(range?.to);
|
|
3152
|
+
return {
|
|
3153
|
+
after,
|
|
3154
|
+
before,
|
|
3155
|
+
timezone: range?.timezone
|
|
3156
|
+
};
|
|
3157
|
+
}
|
|
3158
|
+
function buildAnnotationDateQuery(range) {
|
|
3159
|
+
const dateFrom = formatDate(range?.from);
|
|
3160
|
+
const dateTo = formatDate(range?.to);
|
|
3161
|
+
return {
|
|
3162
|
+
date_from: dateFrom,
|
|
3163
|
+
date_to: dateTo,
|
|
3164
|
+
timezone: range?.timezone
|
|
3165
|
+
};
|
|
3166
|
+
}
|
|
3167
|
+
function formatDate(value) {
|
|
3168
|
+
if (!value)
|
|
3169
|
+
return;
|
|
3170
|
+
return typeof value === "string" ? value : value.toISOString();
|
|
3171
|
+
}
|
|
3172
|
+
|
|
3173
|
+
// src/impls/posthog-utils.ts
|
|
3174
|
+
function normalizeHost(host) {
|
|
3175
|
+
return host.replace(/\/$/, "");
|
|
3176
|
+
}
|
|
3177
|
+
function buildUrl(host, path, query) {
|
|
3178
|
+
if (/^https?:\/\//.test(path)) {
|
|
3179
|
+
return appendQuery(path, query);
|
|
3180
|
+
}
|
|
3181
|
+
const normalizedPath = path.replace(/^\/+/, "");
|
|
3182
|
+
return appendQuery(`${host}/${normalizedPath}`, query);
|
|
3183
|
+
}
|
|
3184
|
+
function appendQuery(url, query) {
|
|
3185
|
+
if (!query)
|
|
3186
|
+
return url;
|
|
3187
|
+
const params = new URLSearchParams;
|
|
3188
|
+
Object.entries(query).forEach(([key, value]) => {
|
|
3189
|
+
if (value === undefined)
|
|
3190
|
+
return;
|
|
3191
|
+
params.set(key, String(value));
|
|
3192
|
+
});
|
|
3193
|
+
const suffix = params.toString();
|
|
3194
|
+
return suffix ? `${url}?${suffix}` : url;
|
|
3195
|
+
}
|
|
3196
|
+
function parseJson(value) {
|
|
3197
|
+
if (!value)
|
|
3198
|
+
return {};
|
|
3199
|
+
try {
|
|
3200
|
+
return JSON.parse(value);
|
|
3201
|
+
} catch {
|
|
3202
|
+
return value;
|
|
3203
|
+
}
|
|
3204
|
+
}
|
|
3205
|
+
|
|
3206
|
+
// src/impls/posthog.ts
|
|
3207
|
+
import { PostHog } from "posthog-node";
|
|
3208
|
+
var DEFAULT_POSTHOG_HOST = "https://app.posthog.com";
|
|
3209
|
+
|
|
3210
|
+
class PosthogAnalyticsProvider {
|
|
3211
|
+
host;
|
|
3212
|
+
projectId;
|
|
3213
|
+
projectApiKey;
|
|
3214
|
+
personalApiKey;
|
|
3215
|
+
mcpUrl;
|
|
3216
|
+
fetchFn;
|
|
3217
|
+
client;
|
|
3218
|
+
reader;
|
|
3219
|
+
constructor(options) {
|
|
3220
|
+
this.host = normalizeHost(options.host ?? DEFAULT_POSTHOG_HOST);
|
|
3221
|
+
this.projectId = options.projectId;
|
|
3222
|
+
this.projectApiKey = options.projectApiKey;
|
|
3223
|
+
this.personalApiKey = options.personalApiKey;
|
|
3224
|
+
this.mcpUrl = options.mcpUrl;
|
|
3225
|
+
this.fetchFn = options.fetch ?? fetch;
|
|
3226
|
+
this.client = options.client ?? (options.projectApiKey ? new PostHog(options.projectApiKey, {
|
|
3227
|
+
host: this.host,
|
|
3228
|
+
requestTimeout: options.requestTimeoutMs ?? 1e4
|
|
3229
|
+
}) : undefined);
|
|
3230
|
+
this.reader = new PosthogAnalyticsReader({
|
|
3231
|
+
projectId: this.projectId,
|
|
3232
|
+
client: { request: this.request.bind(this) }
|
|
3233
|
+
});
|
|
3234
|
+
}
|
|
3235
|
+
async capture(event) {
|
|
3236
|
+
if (!this.client) {
|
|
3237
|
+
throw new Error("PostHog projectApiKey is required for capture.");
|
|
3238
|
+
}
|
|
3239
|
+
await this.client.capture({
|
|
3240
|
+
distinctId: event.distinctId,
|
|
3241
|
+
event: event.event,
|
|
3242
|
+
properties: event.properties,
|
|
3243
|
+
timestamp: event.timestamp,
|
|
3244
|
+
groups: event.groups
|
|
3245
|
+
});
|
|
3246
|
+
}
|
|
3247
|
+
async identify(input) {
|
|
3248
|
+
if (!this.client) {
|
|
3249
|
+
throw new Error("PostHog projectApiKey is required for identify.");
|
|
3250
|
+
}
|
|
3251
|
+
await this.client.identify({
|
|
3252
|
+
distinctId: input.distinctId,
|
|
3253
|
+
properties: {
|
|
3254
|
+
...input.properties ? { $set: input.properties } : {},
|
|
3255
|
+
...input.setOnce ? { $set_once: input.setOnce } : {}
|
|
3256
|
+
}
|
|
3257
|
+
});
|
|
3258
|
+
}
|
|
3259
|
+
async queryHogQL(input) {
|
|
3260
|
+
return this.reader.queryHogQL(input);
|
|
3261
|
+
}
|
|
3262
|
+
async getEvents(input) {
|
|
3263
|
+
return this.reader.getEvents(input);
|
|
3264
|
+
}
|
|
3265
|
+
async getPersons(input) {
|
|
3266
|
+
return this.reader.getPersons(input);
|
|
3267
|
+
}
|
|
3268
|
+
async getInsights(input) {
|
|
3269
|
+
return this.reader.getInsights(input);
|
|
3270
|
+
}
|
|
3271
|
+
async getInsightResult(input) {
|
|
3272
|
+
return this.reader.getInsightResult(input);
|
|
3273
|
+
}
|
|
3274
|
+
async getCohorts(input) {
|
|
3275
|
+
return this.reader.getCohorts(input);
|
|
3276
|
+
}
|
|
3277
|
+
async getFeatureFlags(input) {
|
|
3278
|
+
return this.reader.getFeatureFlags(input);
|
|
3279
|
+
}
|
|
3280
|
+
async getAnnotations(input) {
|
|
3281
|
+
return this.reader.getAnnotations(input);
|
|
3282
|
+
}
|
|
3283
|
+
async request(request) {
|
|
3284
|
+
if (!this.personalApiKey) {
|
|
3285
|
+
throw new Error("PostHog personalApiKey is required for API requests.");
|
|
3286
|
+
}
|
|
3287
|
+
const url = buildUrl(this.host, request.path, request.query);
|
|
3288
|
+
const response = await this.fetchFn(url, {
|
|
3289
|
+
method: request.method,
|
|
3290
|
+
headers: {
|
|
3291
|
+
Authorization: `Bearer ${this.personalApiKey}`,
|
|
3292
|
+
"Content-Type": "application/json",
|
|
3293
|
+
...request.headers ?? {}
|
|
3294
|
+
},
|
|
3295
|
+
body: request.body ? JSON.stringify(request.body) : undefined
|
|
3296
|
+
});
|
|
3297
|
+
const text = await response.text();
|
|
3298
|
+
const data = parseJson(text);
|
|
3299
|
+
return {
|
|
3300
|
+
status: response.status,
|
|
3301
|
+
data,
|
|
3302
|
+
headers: Object.fromEntries(response.headers.entries())
|
|
3303
|
+
};
|
|
3304
|
+
}
|
|
3305
|
+
async callMcpTool(call) {
|
|
3306
|
+
if (!this.mcpUrl) {
|
|
3307
|
+
throw new Error("PostHog MCP URL is not configured.");
|
|
3308
|
+
}
|
|
3309
|
+
const response = await this.fetchFn(this.mcpUrl, {
|
|
3310
|
+
method: "POST",
|
|
3311
|
+
headers: {
|
|
3312
|
+
"Content-Type": "application/json"
|
|
3313
|
+
},
|
|
3314
|
+
body: JSON.stringify({
|
|
3315
|
+
jsonrpc: "2.0",
|
|
3316
|
+
id: 1,
|
|
3317
|
+
method: "tools/call",
|
|
3318
|
+
params: {
|
|
3319
|
+
name: call.name,
|
|
3320
|
+
arguments: call.arguments ?? {}
|
|
3321
|
+
}
|
|
3322
|
+
})
|
|
3323
|
+
});
|
|
3324
|
+
if (!response.ok) {
|
|
3325
|
+
const body = await response.text();
|
|
3326
|
+
throw new Error(`PostHog MCP error (${response.status}): ${body}`);
|
|
3327
|
+
}
|
|
3328
|
+
const result = await response.json();
|
|
3329
|
+
if (result.error) {
|
|
3330
|
+
throw new Error(result.error.message ?? "PostHog MCP error");
|
|
3331
|
+
}
|
|
3332
|
+
return result.result ?? null;
|
|
3333
|
+
}
|
|
3334
|
+
}
|
|
3335
|
+
|
|
3336
|
+
// src/impls/twilio-sms.ts
|
|
3337
|
+
import Twilio from "twilio";
|
|
3338
|
+
|
|
3339
|
+
class TwilioSmsProvider {
|
|
3340
|
+
client;
|
|
3341
|
+
fromNumber;
|
|
3342
|
+
constructor(options) {
|
|
3343
|
+
this.client = options.client ?? Twilio(options.accountSid, options.authToken);
|
|
3344
|
+
this.fromNumber = options.fromNumber;
|
|
3345
|
+
}
|
|
3346
|
+
async sendSms(input) {
|
|
3347
|
+
const message = await this.client.messages.create({
|
|
3348
|
+
to: input.to,
|
|
3349
|
+
from: input.from ?? this.fromNumber,
|
|
3350
|
+
body: input.body
|
|
3351
|
+
});
|
|
3352
|
+
return {
|
|
3353
|
+
id: message.sid,
|
|
3354
|
+
to: message.to ?? input.to,
|
|
3355
|
+
from: message.from ?? input.from ?? this.fromNumber ?? "",
|
|
3356
|
+
body: message.body ?? input.body,
|
|
3357
|
+
status: mapStatus(message.status),
|
|
3358
|
+
sentAt: message.dateCreated ? new Date(message.dateCreated) : undefined,
|
|
3359
|
+
deliveredAt: message.status === "delivered" && message.dateUpdated ? new Date(message.dateUpdated) : undefined,
|
|
3360
|
+
price: message.price ? Number(message.price) : undefined,
|
|
3361
|
+
priceCurrency: message.priceUnit ?? undefined,
|
|
3362
|
+
errorCode: message.errorCode ? String(message.errorCode) : undefined,
|
|
3363
|
+
errorMessage: message.errorMessage ?? undefined
|
|
3364
|
+
};
|
|
3365
|
+
}
|
|
3366
|
+
async getDeliveryStatus(messageId) {
|
|
3367
|
+
const message = await this.client.messages(messageId).fetch();
|
|
3368
|
+
return {
|
|
3369
|
+
status: mapStatus(message.status),
|
|
3370
|
+
errorCode: message.errorCode ? String(message.errorCode) : undefined,
|
|
3371
|
+
errorMessage: message.errorMessage ?? undefined,
|
|
3372
|
+
updatedAt: message.dateUpdated ? new Date(message.dateUpdated) : new Date
|
|
3373
|
+
};
|
|
3374
|
+
}
|
|
3375
|
+
}
|
|
3376
|
+
function mapStatus(status) {
|
|
3377
|
+
switch (status) {
|
|
3378
|
+
case "queued":
|
|
3379
|
+
case "accepted":
|
|
3380
|
+
case "scheduled":
|
|
3381
|
+
return "queued";
|
|
3382
|
+
case "sending":
|
|
3383
|
+
case "processing":
|
|
3384
|
+
return "sending";
|
|
3385
|
+
case "sent":
|
|
3386
|
+
return "sent";
|
|
3387
|
+
case "delivered":
|
|
3388
|
+
return "delivered";
|
|
3389
|
+
case "undelivered":
|
|
3390
|
+
return "undelivered";
|
|
3391
|
+
case "failed":
|
|
3392
|
+
case "canceled":
|
|
3393
|
+
return "failed";
|
|
3394
|
+
default:
|
|
3395
|
+
return "queued";
|
|
3396
|
+
}
|
|
3397
|
+
}
|
|
3398
|
+
|
|
3399
|
+
// src/impls/powens-client.ts
|
|
3400
|
+
import { URL } from "node:url";
|
|
3401
|
+
var POWENS_BASE_URL = {
|
|
3402
|
+
sandbox: "https://api-sandbox.powens.com/v2",
|
|
3403
|
+
production: "https://api.powens.com/v2"
|
|
3404
|
+
};
|
|
3405
|
+
|
|
3406
|
+
class PowensClientError extends Error {
|
|
3407
|
+
status;
|
|
3408
|
+
code;
|
|
3409
|
+
requestId;
|
|
3410
|
+
response;
|
|
3411
|
+
constructor(message, status, code, requestId, response) {
|
|
3412
|
+
super(message);
|
|
3413
|
+
this.name = "PowensClientError";
|
|
3414
|
+
this.status = status;
|
|
3415
|
+
this.code = code;
|
|
3416
|
+
this.requestId = requestId;
|
|
3417
|
+
this.response = response;
|
|
3418
|
+
}
|
|
3419
|
+
}
|
|
3420
|
+
|
|
3421
|
+
class PowensClient {
|
|
3422
|
+
clientId;
|
|
3423
|
+
clientSecret;
|
|
3424
|
+
apiKey;
|
|
3425
|
+
fetchImpl;
|
|
3426
|
+
logger;
|
|
3427
|
+
defaultTimeoutMs;
|
|
3428
|
+
token;
|
|
3429
|
+
baseUrl;
|
|
3430
|
+
constructor(options) {
|
|
3431
|
+
this.clientId = options.clientId;
|
|
3432
|
+
this.clientSecret = options.clientSecret;
|
|
3433
|
+
this.apiKey = options.apiKey;
|
|
3434
|
+
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
3435
|
+
this.logger = options.logger;
|
|
3436
|
+
this.defaultTimeoutMs = options.defaultTimeoutMs ?? 15000;
|
|
3437
|
+
this.baseUrl = options.baseUrl ?? POWENS_BASE_URL[options.environment] ?? POWENS_BASE_URL.production;
|
|
3438
|
+
}
|
|
3439
|
+
async listAccounts(params) {
|
|
3440
|
+
const searchParams = {
|
|
3441
|
+
cursor: params.cursor,
|
|
3442
|
+
limit: params.limit,
|
|
3443
|
+
include_balances: params.includeBalances,
|
|
3444
|
+
institution_uuid: params.institutionUuid
|
|
3445
|
+
};
|
|
3446
|
+
const response = await this.request({
|
|
3447
|
+
method: "GET",
|
|
3448
|
+
path: `/users/${encodeURIComponent(params.userUuid)}/accounts`,
|
|
3449
|
+
searchParams
|
|
3450
|
+
});
|
|
3451
|
+
return response;
|
|
3452
|
+
}
|
|
3453
|
+
async getAccount(accountUuid) {
|
|
3454
|
+
return this.request({
|
|
3455
|
+
method: "GET",
|
|
3456
|
+
path: `/accounts/${encodeURIComponent(accountUuid)}`
|
|
3457
|
+
});
|
|
3458
|
+
}
|
|
3459
|
+
async listTransactions(params) {
|
|
3460
|
+
const searchParams = {
|
|
3461
|
+
cursor: params.cursor,
|
|
3462
|
+
limit: params.limit,
|
|
3463
|
+
from: params.from,
|
|
3464
|
+
to: params.to,
|
|
3465
|
+
include_pending: params.includePending
|
|
3466
|
+
};
|
|
3467
|
+
return this.request({
|
|
3468
|
+
method: "GET",
|
|
3469
|
+
path: `/accounts/${encodeURIComponent(params.accountUuid)}/transactions`,
|
|
3470
|
+
searchParams
|
|
3471
|
+
});
|
|
3472
|
+
}
|
|
3473
|
+
async getBalances(accountUuid) {
|
|
3474
|
+
return this.request({
|
|
3475
|
+
method: "GET",
|
|
3476
|
+
path: `/accounts/${encodeURIComponent(accountUuid)}/balances`
|
|
3477
|
+
});
|
|
3478
|
+
}
|
|
3479
|
+
async getConnectionStatus(connectionUuid) {
|
|
3480
|
+
return this.request({
|
|
3481
|
+
method: "GET",
|
|
3482
|
+
path: `/connections/${encodeURIComponent(connectionUuid)}`
|
|
3483
|
+
});
|
|
3484
|
+
}
|
|
3485
|
+
async request(options) {
|
|
3486
|
+
const url = new URL(options.path, this.baseUrl);
|
|
3487
|
+
if (options.searchParams) {
|
|
3488
|
+
for (const [key, value] of Object.entries(options.searchParams)) {
|
|
3489
|
+
if (value === undefined || value === null)
|
|
3490
|
+
continue;
|
|
3491
|
+
url.searchParams.set(key, String(value));
|
|
3492
|
+
}
|
|
3493
|
+
}
|
|
3494
|
+
const headers = {
|
|
3495
|
+
Accept: "application/json",
|
|
3496
|
+
"Content-Type": "application/json",
|
|
3497
|
+
...options.headers
|
|
3498
|
+
};
|
|
3499
|
+
if (this.apiKey) {
|
|
3500
|
+
headers["x-api-key"] = this.apiKey;
|
|
3501
|
+
}
|
|
3502
|
+
if (!options.skipAuth) {
|
|
3503
|
+
const token = await this.ensureAccessToken();
|
|
3504
|
+
headers.Authorization = `Bearer ${token}`;
|
|
3505
|
+
}
|
|
3506
|
+
const controller = new AbortController;
|
|
3507
|
+
const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? this.defaultTimeoutMs);
|
|
3508
|
+
try {
|
|
3509
|
+
const response = await this.fetchImpl(url, {
|
|
3510
|
+
method: options.method,
|
|
3511
|
+
headers,
|
|
3512
|
+
body: options.body ? JSON.stringify(options.body) : undefined,
|
|
3513
|
+
signal: controller.signal
|
|
3514
|
+
});
|
|
3515
|
+
const requestId = response.headers.get("x-request-id") ?? undefined;
|
|
3516
|
+
if (!response.ok) {
|
|
3517
|
+
let errorBody;
|
|
3518
|
+
try {
|
|
3519
|
+
errorBody = await response.json();
|
|
3520
|
+
} catch {}
|
|
3521
|
+
const errorObject = typeof errorBody === "object" && errorBody !== null ? errorBody : undefined;
|
|
3522
|
+
const message = typeof errorObject?.message === "string" ? errorObject.message : `Powens API request failed with status ${response.status}`;
|
|
3523
|
+
const code = typeof errorObject?.code === "string" ? errorObject.code : undefined;
|
|
3524
|
+
throw new PowensClientError(message, response.status, code, requestId, errorBody);
|
|
3525
|
+
}
|
|
3526
|
+
if (response.status === 204) {
|
|
3527
|
+
return;
|
|
3528
|
+
}
|
|
3529
|
+
try {
|
|
3530
|
+
return await response.json();
|
|
3531
|
+
} catch (error) {
|
|
3532
|
+
this.logger?.error?.("[PowensClient] Failed to parse JSON response", error);
|
|
3533
|
+
throw new PowensClientError("Failed to parse Powens response payload as JSON", response.status, undefined, requestId);
|
|
3534
|
+
}
|
|
3535
|
+
} catch (error) {
|
|
3536
|
+
if (error instanceof PowensClientError) {
|
|
3537
|
+
throw error;
|
|
3538
|
+
}
|
|
3539
|
+
if (error.name === "AbortError") {
|
|
3540
|
+
throw new PowensClientError(`Powens API request timed out after ${options.timeoutMs ?? this.defaultTimeoutMs}ms`, 408);
|
|
3541
|
+
}
|
|
3542
|
+
this.logger?.error?.("[PowensClient] Request failed", error);
|
|
3543
|
+
throw new PowensClientError(error.message ?? "Powens API request failed", 500);
|
|
3544
|
+
} finally {
|
|
3545
|
+
clearTimeout(timeout);
|
|
3546
|
+
}
|
|
3547
|
+
}
|
|
3548
|
+
async ensureAccessToken() {
|
|
3549
|
+
if (this.token && Date.now() < this.token.expiresAt - 5000) {
|
|
3550
|
+
return this.token.accessToken;
|
|
3551
|
+
}
|
|
3552
|
+
this.token = await this.fetchAccessToken();
|
|
3553
|
+
return this.token.accessToken;
|
|
3554
|
+
}
|
|
3555
|
+
async fetchAccessToken() {
|
|
3556
|
+
const url = new URL("/oauth/token", this.baseUrl);
|
|
3557
|
+
const basicAuth = Buffer.from(`${this.clientId}:${this.clientSecret}`, "utf-8").toString("base64");
|
|
3558
|
+
const response = await this.fetchImpl(url, {
|
|
3559
|
+
method: "POST",
|
|
3560
|
+
headers: {
|
|
3561
|
+
Authorization: `Basic ${basicAuth}`,
|
|
3562
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
3563
|
+
Accept: "application/json"
|
|
3564
|
+
},
|
|
3565
|
+
body: new URLSearchParams({
|
|
3566
|
+
grant_type: "client_credentials"
|
|
3567
|
+
}).toString()
|
|
3568
|
+
});
|
|
3569
|
+
if (!response.ok) {
|
|
3570
|
+
let errorBody;
|
|
3571
|
+
try {
|
|
3572
|
+
errorBody = await response.json();
|
|
3573
|
+
} catch {}
|
|
3574
|
+
const errorObject = typeof errorBody === "object" && errorBody !== null ? errorBody : undefined;
|
|
3575
|
+
const message = typeof errorObject?.error_description === "string" ? errorObject.error_description : "Failed to obtain Powens access token";
|
|
3576
|
+
throw new PowensClientError(message, response.status, undefined, undefined, errorBody);
|
|
3577
|
+
}
|
|
3578
|
+
const payload = await response.json();
|
|
3579
|
+
const expiresAt = Date.now() + payload.expires_in * 1000;
|
|
3580
|
+
this.logger?.debug?.("[PowensClient] Received access token", {
|
|
3581
|
+
expiresIn: payload.expires_in
|
|
3582
|
+
});
|
|
3583
|
+
return {
|
|
3584
|
+
accessToken: payload.access_token,
|
|
3585
|
+
expiresAt,
|
|
3586
|
+
scope: payload.scope
|
|
3587
|
+
};
|
|
3588
|
+
}
|
|
3589
|
+
}
|
|
3590
|
+
|
|
3591
|
+
// src/impls/powens-openbanking.ts
|
|
3592
|
+
class PowensOpenBankingProvider {
|
|
3593
|
+
client;
|
|
3594
|
+
logger;
|
|
3595
|
+
constructor(options) {
|
|
3596
|
+
this.client = new PowensClient(options);
|
|
3597
|
+
this.logger = options.logger;
|
|
3598
|
+
}
|
|
3599
|
+
async listAccounts(params) {
|
|
3600
|
+
if (!params.userId) {
|
|
3601
|
+
throw new PowensClientError("Powens account listing requires the upstream userId mapped to Powens user UUID.", 400);
|
|
3602
|
+
}
|
|
3603
|
+
const context = this.toContext(params.tenantId, params.connectionId);
|
|
3604
|
+
try {
|
|
3605
|
+
const response = await this.client.listAccounts({
|
|
3606
|
+
userUuid: params.userId,
|
|
3607
|
+
cursor: params.cursor,
|
|
3608
|
+
limit: params.pageSize,
|
|
3609
|
+
includeBalances: params.includeBalances,
|
|
3610
|
+
institutionUuid: params.institutionId
|
|
3611
|
+
});
|
|
3612
|
+
return {
|
|
3613
|
+
accounts: response.accounts.map((account) => this.mapAccount(account, context)),
|
|
3614
|
+
nextCursor: response.pagination?.nextCursor,
|
|
3615
|
+
hasMore: response.pagination?.hasMore
|
|
3616
|
+
};
|
|
3617
|
+
} catch (error) {
|
|
3618
|
+
this.handleError("listAccounts", error);
|
|
3619
|
+
}
|
|
3620
|
+
}
|
|
3621
|
+
async getAccountDetails(params) {
|
|
3622
|
+
const context = this.toContext(params.tenantId, params.connectionId);
|
|
3623
|
+
try {
|
|
3624
|
+
const account = await this.client.getAccount(params.accountId);
|
|
3625
|
+
return this.mapAccountDetails(account, context);
|
|
3626
|
+
} catch (error) {
|
|
3627
|
+
this.handleError("getAccountDetails", error);
|
|
3628
|
+
}
|
|
3629
|
+
}
|
|
3630
|
+
async listTransactions(params) {
|
|
3631
|
+
const context = this.toContext(params.tenantId, params.connectionId);
|
|
3632
|
+
try {
|
|
3633
|
+
const response = await this.client.listTransactions({
|
|
3634
|
+
accountUuid: params.accountId,
|
|
3635
|
+
cursor: params.cursor,
|
|
3636
|
+
limit: params.pageSize,
|
|
3637
|
+
from: params.from,
|
|
3638
|
+
to: params.to,
|
|
3639
|
+
includePending: params.includePending
|
|
3640
|
+
});
|
|
3641
|
+
return {
|
|
3642
|
+
transactions: response.transactions.map((transaction) => this.mapTransaction(transaction, context)),
|
|
3643
|
+
nextCursor: response.pagination?.nextCursor,
|
|
3644
|
+
hasMore: response.pagination?.hasMore
|
|
3645
|
+
};
|
|
3646
|
+
} catch (error) {
|
|
3647
|
+
this.handleError("listTransactions", error);
|
|
3648
|
+
}
|
|
3649
|
+
}
|
|
3650
|
+
async getBalances(params) {
|
|
3651
|
+
const context = this.toContext(params.tenantId, params.connectionId);
|
|
3652
|
+
try {
|
|
3653
|
+
const balances = await this.client.getBalances(params.accountId);
|
|
3654
|
+
return balances.filter((balance) => params.balanceTypes?.length ? params.balanceTypes.includes(String(balance.type)) : true).map((balance) => this.mapBalance(balance, context));
|
|
3655
|
+
} catch (error) {
|
|
3656
|
+
this.handleError("getBalances", error);
|
|
3657
|
+
}
|
|
3658
|
+
}
|
|
3659
|
+
async getConnectionStatus(params) {
|
|
3660
|
+
try {
|
|
3661
|
+
const status = await this.client.getConnectionStatus(params.connectionId);
|
|
3662
|
+
return {
|
|
3663
|
+
connectionId: params.connectionId,
|
|
3664
|
+
tenantId: params.tenantId,
|
|
3665
|
+
status: this.mapConnectionStatus(status.status),
|
|
3666
|
+
lastCheckedAt: status.lastAttemptAt,
|
|
3667
|
+
errorCode: status.errorCode,
|
|
3668
|
+
errorMessage: status.errorMessage,
|
|
3669
|
+
details: status.metadata
|
|
3670
|
+
};
|
|
3671
|
+
} catch (error) {
|
|
3672
|
+
this.handleError("getConnectionStatus", error);
|
|
3673
|
+
}
|
|
3674
|
+
}
|
|
3675
|
+
mapAccount(account, context) {
|
|
3676
|
+
return {
|
|
3677
|
+
id: account.uuid,
|
|
3678
|
+
externalId: account.reference ?? account.uuid,
|
|
3679
|
+
tenantId: context.tenantId,
|
|
3680
|
+
connectionId: context.connectionId,
|
|
3681
|
+
userId: account.userUuid,
|
|
3682
|
+
displayName: account.name,
|
|
3683
|
+
institutionId: account.institution.id,
|
|
3684
|
+
institutionName: account.institution.name,
|
|
3685
|
+
institutionLogoUrl: account.institution.logoUrl,
|
|
3686
|
+
accountType: account.type ?? "unknown",
|
|
3687
|
+
iban: account.iban,
|
|
3688
|
+
bic: account.bic,
|
|
3689
|
+
currency: account.currency ?? "EUR",
|
|
3690
|
+
accountNumberMasked: account.metadata?.account_number_masked,
|
|
3691
|
+
ownership: this.mapOwnership(account.metadata?.ownership),
|
|
3692
|
+
status: this.mapAccountStatus(account.status),
|
|
3693
|
+
lastSyncedAt: account.metadata?.last_sync_at,
|
|
3694
|
+
metadata: account.metadata
|
|
3695
|
+
};
|
|
3696
|
+
}
|
|
3697
|
+
mapAccountDetails(account, context) {
|
|
3698
|
+
return {
|
|
3699
|
+
...this.mapAccount(account, context),
|
|
3700
|
+
productCode: account.metadata?.product_code,
|
|
3701
|
+
openedAt: account.metadata?.opened_at,
|
|
3702
|
+
closedAt: account.metadata?.closed_at,
|
|
3703
|
+
availableBalance: account.availableBalance ?? undefined,
|
|
3704
|
+
currentBalance: account.balance ?? undefined,
|
|
3705
|
+
creditLimit: account.metadata?.credit_limit,
|
|
3706
|
+
customFields: account.metadata
|
|
3707
|
+
};
|
|
3708
|
+
}
|
|
3709
|
+
mapTransaction(transaction, context) {
|
|
3710
|
+
return {
|
|
3711
|
+
id: transaction.uuid,
|
|
3712
|
+
externalId: transaction.uuid,
|
|
3713
|
+
tenantId: context.tenantId,
|
|
3714
|
+
accountId: transaction.accountUuid,
|
|
3715
|
+
connectionId: context.connectionId,
|
|
3716
|
+
amount: transaction.amount,
|
|
3717
|
+
currency: transaction.currency,
|
|
3718
|
+
direction: transaction.direction === "credit" ? "credit" : "debit",
|
|
3719
|
+
description: transaction.description ?? transaction.rawLabel,
|
|
3720
|
+
bookingDate: transaction.bookingDate,
|
|
3721
|
+
valueDate: transaction.valueDate,
|
|
3722
|
+
postedAt: transaction.bookingDate,
|
|
3723
|
+
category: transaction.category,
|
|
3724
|
+
rawCategory: transaction.rawLabel,
|
|
3725
|
+
merchantName: transaction.merchantName,
|
|
3726
|
+
merchantCategoryCode: transaction.merchantCategoryCode,
|
|
3727
|
+
counterpartyName: transaction.counterpartyName,
|
|
3728
|
+
counterpartyAccount: transaction.counterpartyAccount,
|
|
3729
|
+
reference: transaction.metadata?.reference,
|
|
3730
|
+
status: this.mapTransactionStatus(transaction.status),
|
|
3731
|
+
metadata: transaction.metadata
|
|
3732
|
+
};
|
|
3733
|
+
}
|
|
3734
|
+
mapBalance(balance, context) {
|
|
3735
|
+
return {
|
|
3736
|
+
accountId: balance.accountUuid,
|
|
3737
|
+
connectionId: context.connectionId,
|
|
3738
|
+
tenantId: context.tenantId,
|
|
3739
|
+
type: balance.type ?? "current",
|
|
3740
|
+
currency: balance.currency ?? "EUR",
|
|
3741
|
+
amount: balance.amount,
|
|
3742
|
+
lastUpdatedAt: balance.updatedAt,
|
|
3743
|
+
metadata: balance.metadata
|
|
3744
|
+
};
|
|
3745
|
+
}
|
|
3746
|
+
toContext(tenantId, connectionId) {
|
|
3747
|
+
return { tenantId, connectionId };
|
|
3748
|
+
}
|
|
3749
|
+
mapOwnership(value) {
|
|
3750
|
+
switch (value?.toLowerCase()) {
|
|
3751
|
+
case "individual":
|
|
3752
|
+
case "personal":
|
|
3753
|
+
return "individual";
|
|
3754
|
+
case "joint":
|
|
3755
|
+
return "joint";
|
|
3756
|
+
case "business":
|
|
3757
|
+
case "corporate":
|
|
3758
|
+
return "business";
|
|
3759
|
+
default:
|
|
3760
|
+
return "unknown";
|
|
3761
|
+
}
|
|
3762
|
+
}
|
|
3763
|
+
mapAccountStatus(status) {
|
|
3764
|
+
switch (status?.toLowerCase()) {
|
|
3765
|
+
case "active":
|
|
3766
|
+
case "enabled":
|
|
3767
|
+
return "active";
|
|
3768
|
+
case "disabled":
|
|
3769
|
+
case "inactive":
|
|
3770
|
+
return "inactive";
|
|
3771
|
+
case "closed":
|
|
3772
|
+
return "closed";
|
|
3773
|
+
case "suspended":
|
|
3774
|
+
return "suspended";
|
|
3775
|
+
default:
|
|
3776
|
+
return "active";
|
|
3777
|
+
}
|
|
3778
|
+
}
|
|
3779
|
+
mapTransactionStatus(status) {
|
|
3780
|
+
switch (status?.toLowerCase()) {
|
|
3781
|
+
case "pending":
|
|
3782
|
+
case "authorised":
|
|
3783
|
+
return "pending";
|
|
3784
|
+
case "booked":
|
|
3785
|
+
case "posted":
|
|
3786
|
+
return "booked";
|
|
3787
|
+
case "cancelled":
|
|
3788
|
+
case "rejected":
|
|
3789
|
+
return "cancelled";
|
|
3790
|
+
default:
|
|
3791
|
+
return "booked";
|
|
3792
|
+
}
|
|
3793
|
+
}
|
|
3794
|
+
mapConnectionStatus(status) {
|
|
3795
|
+
switch (status) {
|
|
3796
|
+
case "healthy":
|
|
3797
|
+
return "healthy";
|
|
3798
|
+
case "pending":
|
|
3799
|
+
return "degraded";
|
|
3800
|
+
case "error":
|
|
3801
|
+
return "error";
|
|
3802
|
+
case "revoked":
|
|
3803
|
+
return "disconnected";
|
|
3804
|
+
default:
|
|
3805
|
+
return "degraded";
|
|
3806
|
+
}
|
|
3807
|
+
}
|
|
3808
|
+
handleError(operation, error) {
|
|
3809
|
+
if (error instanceof PowensClientError) {
|
|
3810
|
+
this.logger?.error?.(`[PowensOpenBankingProvider] ${operation} failed`, {
|
|
3811
|
+
status: error.status,
|
|
3812
|
+
code: error.code,
|
|
3813
|
+
requestId: error.requestId,
|
|
3814
|
+
message: error.message
|
|
3815
|
+
});
|
|
3816
|
+
throw error;
|
|
3817
|
+
}
|
|
3818
|
+
this.logger?.error?.(`[PowensOpenBankingProvider] ${operation} failed with unexpected error`, error);
|
|
3819
|
+
throw error instanceof Error ? error : new Error(`Powens operation "${operation}" failed`);
|
|
3820
|
+
}
|
|
3821
|
+
}
|
|
3822
|
+
|
|
3823
|
+
// src/impls/linear.ts
|
|
3824
|
+
import { LinearClient } from "@linear/sdk";
|
|
3825
|
+
var PRIORITY_MAP = {
|
|
3826
|
+
urgent: 1,
|
|
3827
|
+
high: 2,
|
|
3828
|
+
medium: 3,
|
|
3829
|
+
low: 4,
|
|
3830
|
+
none: 0
|
|
3831
|
+
};
|
|
3832
|
+
|
|
3833
|
+
class LinearProjectManagementProvider {
|
|
3834
|
+
client;
|
|
3835
|
+
defaults;
|
|
3836
|
+
constructor(options) {
|
|
3837
|
+
this.client = options.client ?? new LinearClient({ apiKey: options.apiKey });
|
|
3838
|
+
this.defaults = {
|
|
3839
|
+
teamId: options.teamId,
|
|
3840
|
+
projectId: options.projectId,
|
|
3841
|
+
assigneeId: options.assigneeId,
|
|
3842
|
+
stateId: options.stateId,
|
|
3843
|
+
labelIds: options.labelIds,
|
|
3844
|
+
tagLabelMap: options.tagLabelMap
|
|
3845
|
+
};
|
|
3846
|
+
}
|
|
3847
|
+
async createWorkItem(input) {
|
|
3848
|
+
const teamId = this.defaults.teamId;
|
|
3849
|
+
if (!teamId) {
|
|
3850
|
+
throw new Error("Linear teamId is required to create work items.");
|
|
3851
|
+
}
|
|
3852
|
+
const payload = await this.client.createIssue({
|
|
3853
|
+
teamId,
|
|
3854
|
+
title: input.title,
|
|
3855
|
+
description: input.description,
|
|
3856
|
+
priority: mapPriority(input.priority),
|
|
3857
|
+
estimate: input.estimate,
|
|
3858
|
+
assigneeId: input.assigneeId ?? this.defaults.assigneeId,
|
|
3859
|
+
projectId: input.projectId ?? this.defaults.projectId,
|
|
3860
|
+
stateId: this.defaults.stateId,
|
|
3861
|
+
labelIds: resolveLabelIds(this.defaults, input.tags)
|
|
3862
|
+
});
|
|
3863
|
+
const issue = await payload.issue;
|
|
3864
|
+
const state = issue ? await issue.state : undefined;
|
|
3865
|
+
return {
|
|
3866
|
+
id: issue?.id ?? "",
|
|
3867
|
+
title: issue?.title ?? input.title,
|
|
3868
|
+
url: issue?.url ?? undefined,
|
|
3869
|
+
status: state?.name ?? undefined,
|
|
3870
|
+
priority: input.priority,
|
|
3871
|
+
tags: input.tags,
|
|
3872
|
+
projectId: input.projectId ?? this.defaults.projectId,
|
|
3873
|
+
externalId: input.externalId,
|
|
3874
|
+
metadata: input.metadata
|
|
3875
|
+
};
|
|
3876
|
+
}
|
|
3877
|
+
async createWorkItems(items) {
|
|
3878
|
+
const created = [];
|
|
3879
|
+
for (const item of items) {
|
|
3880
|
+
created.push(await this.createWorkItem(item));
|
|
3881
|
+
}
|
|
3882
|
+
return created;
|
|
3883
|
+
}
|
|
3884
|
+
}
|
|
3885
|
+
function mapPriority(priority) {
|
|
3886
|
+
if (!priority)
|
|
3887
|
+
return;
|
|
3888
|
+
return PRIORITY_MAP[priority] ?? undefined;
|
|
3889
|
+
}
|
|
3890
|
+
function resolveLabelIds(defaults, tags) {
|
|
3891
|
+
const labelIds = new Set;
|
|
3892
|
+
(defaults.labelIds ?? []).forEach((id) => labelIds.add(id));
|
|
3893
|
+
if (tags && defaults.tagLabelMap) {
|
|
3894
|
+
tags.forEach((tag) => {
|
|
3895
|
+
const mapped = defaults.tagLabelMap?.[tag];
|
|
3896
|
+
if (mapped)
|
|
3897
|
+
labelIds.add(mapped);
|
|
3898
|
+
});
|
|
3899
|
+
}
|
|
3900
|
+
const merged = [...labelIds];
|
|
3901
|
+
return merged.length > 0 ? merged : undefined;
|
|
3902
|
+
}
|
|
3903
|
+
|
|
3904
|
+
// src/impls/jira.ts
|
|
3905
|
+
import { Buffer as Buffer4 } from "node:buffer";
|
|
3906
|
+
|
|
3907
|
+
class JiraProjectManagementProvider {
|
|
3908
|
+
siteUrl;
|
|
3909
|
+
authHeader;
|
|
3910
|
+
defaults;
|
|
3911
|
+
fetchFn;
|
|
3912
|
+
constructor(options) {
|
|
3913
|
+
this.siteUrl = normalizeSiteUrl(options.siteUrl);
|
|
3914
|
+
this.authHeader = buildAuthHeader(options.email, options.apiToken);
|
|
3915
|
+
this.defaults = {
|
|
3916
|
+
projectKey: options.projectKey,
|
|
3917
|
+
issueType: options.issueType,
|
|
3918
|
+
defaultLabels: options.defaultLabels,
|
|
3919
|
+
issueTypeMap: options.issueTypeMap
|
|
3920
|
+
};
|
|
3921
|
+
this.fetchFn = options.fetch ?? fetch;
|
|
3922
|
+
}
|
|
3923
|
+
async createWorkItem(input) {
|
|
3924
|
+
const projectKey = input.projectId ?? this.defaults.projectKey;
|
|
3925
|
+
if (!projectKey) {
|
|
3926
|
+
throw new Error("Jira projectKey is required to create work items.");
|
|
3927
|
+
}
|
|
3928
|
+
const issueType = resolveIssueType(input.type, this.defaults);
|
|
3929
|
+
const description = buildJiraDescription(input.description);
|
|
3930
|
+
const labels = mergeLabels(this.defaults.defaultLabels, input.tags);
|
|
3931
|
+
const priority = mapPriority2(input.priority);
|
|
3932
|
+
const payload = {
|
|
3933
|
+
fields: {
|
|
3934
|
+
project: { key: projectKey },
|
|
3935
|
+
summary: input.title,
|
|
3936
|
+
description,
|
|
3937
|
+
issuetype: { name: issueType },
|
|
3938
|
+
labels,
|
|
3939
|
+
priority: priority ? { name: priority } : undefined,
|
|
3940
|
+
assignee: input.assigneeId ? { accountId: input.assigneeId } : undefined,
|
|
3941
|
+
duedate: input.dueDate ? input.dueDate.toISOString().slice(0, 10) : undefined
|
|
3942
|
+
}
|
|
3943
|
+
};
|
|
3944
|
+
const response = await this.fetchFn(`${this.siteUrl}/rest/api/3/issue`, {
|
|
3945
|
+
method: "POST",
|
|
3946
|
+
headers: {
|
|
3947
|
+
Authorization: this.authHeader,
|
|
3948
|
+
"Content-Type": "application/json",
|
|
3949
|
+
Accept: "application/json"
|
|
3950
|
+
},
|
|
3951
|
+
body: JSON.stringify(payload)
|
|
3952
|
+
});
|
|
3953
|
+
if (!response.ok) {
|
|
3954
|
+
const body = await response.text();
|
|
3955
|
+
throw new Error(`Jira API error (${response.status}): ${body || response.statusText}`);
|
|
3956
|
+
}
|
|
3957
|
+
const data = await response.json();
|
|
3958
|
+
return {
|
|
3959
|
+
id: data.id ?? data.key ?? "",
|
|
3960
|
+
title: input.title,
|
|
3961
|
+
url: data.key ? `${this.siteUrl}/browse/${data.key}` : undefined,
|
|
3962
|
+
status: input.status,
|
|
3963
|
+
priority: input.priority,
|
|
3964
|
+
tags: input.tags,
|
|
3965
|
+
projectId: projectKey,
|
|
3966
|
+
externalId: input.externalId,
|
|
3967
|
+
metadata: input.metadata
|
|
3968
|
+
};
|
|
3969
|
+
}
|
|
3970
|
+
async createWorkItems(items) {
|
|
3971
|
+
const created = [];
|
|
3972
|
+
for (const item of items) {
|
|
3973
|
+
created.push(await this.createWorkItem(item));
|
|
3974
|
+
}
|
|
3975
|
+
return created;
|
|
3976
|
+
}
|
|
3977
|
+
}
|
|
3978
|
+
function normalizeSiteUrl(siteUrl) {
|
|
3979
|
+
return siteUrl.replace(/\/$/, "");
|
|
3980
|
+
}
|
|
3981
|
+
function buildAuthHeader(email2, apiToken) {
|
|
3982
|
+
const token = Buffer4.from(`${email2}:${apiToken}`).toString("base64");
|
|
3983
|
+
return `Basic ${token}`;
|
|
3984
|
+
}
|
|
3985
|
+
function resolveIssueType(type, defaults) {
|
|
3986
|
+
if (type && defaults.issueTypeMap?.[type]) {
|
|
3987
|
+
return defaults.issueTypeMap[type] ?? defaults.issueType ?? "Task";
|
|
3988
|
+
}
|
|
3989
|
+
return defaults.issueType ?? "Task";
|
|
3990
|
+
}
|
|
3991
|
+
function mapPriority2(priority) {
|
|
3992
|
+
switch (priority) {
|
|
3993
|
+
case "urgent":
|
|
3994
|
+
return "Highest";
|
|
3995
|
+
case "high":
|
|
3996
|
+
return "High";
|
|
3997
|
+
case "medium":
|
|
3998
|
+
return "Medium";
|
|
3999
|
+
case "low":
|
|
4000
|
+
return "Low";
|
|
4001
|
+
case "none":
|
|
4002
|
+
default:
|
|
4003
|
+
return;
|
|
4004
|
+
}
|
|
4005
|
+
}
|
|
4006
|
+
function mergeLabels(defaults, tags) {
|
|
4007
|
+
const merged = new Set;
|
|
4008
|
+
(defaults ?? []).forEach((label) => merged.add(label));
|
|
4009
|
+
(tags ?? []).forEach((tag) => merged.add(tag));
|
|
4010
|
+
const result = [...merged];
|
|
4011
|
+
return result.length > 0 ? result : undefined;
|
|
4012
|
+
}
|
|
4013
|
+
function buildJiraDescription(description) {
|
|
4014
|
+
if (!description)
|
|
4015
|
+
return;
|
|
4016
|
+
const lines = description.split(/\r?\n/).filter((line) => line.trim());
|
|
4017
|
+
const content = lines.map((line) => ({
|
|
4018
|
+
type: "paragraph",
|
|
4019
|
+
content: [{ type: "text", text: line }]
|
|
4020
|
+
}));
|
|
4021
|
+
if (content.length === 0)
|
|
4022
|
+
return;
|
|
4023
|
+
return { type: "doc", version: 1, content };
|
|
4024
|
+
}
|
|
4025
|
+
|
|
4026
|
+
// src/impls/notion.ts
|
|
4027
|
+
import { Client } from "@notionhq/client";
|
|
4028
|
+
|
|
4029
|
+
class NotionProjectManagementProvider {
|
|
4030
|
+
client;
|
|
4031
|
+
defaults;
|
|
4032
|
+
constructor(options) {
|
|
4033
|
+
this.client = options.client ?? new Client({ auth: options.apiKey });
|
|
4034
|
+
this.defaults = {
|
|
4035
|
+
databaseId: options.databaseId,
|
|
4036
|
+
summaryParentPageId: options.summaryParentPageId,
|
|
4037
|
+
titleProperty: options.titleProperty,
|
|
4038
|
+
statusProperty: options.statusProperty,
|
|
4039
|
+
priorityProperty: options.priorityProperty,
|
|
4040
|
+
tagsProperty: options.tagsProperty,
|
|
4041
|
+
dueDateProperty: options.dueDateProperty,
|
|
4042
|
+
descriptionProperty: options.descriptionProperty
|
|
4043
|
+
};
|
|
4044
|
+
}
|
|
4045
|
+
async createWorkItem(input) {
|
|
4046
|
+
if (input.type === "summary" && this.defaults.summaryParentPageId) {
|
|
4047
|
+
return this.createSummaryPage(input);
|
|
4048
|
+
}
|
|
4049
|
+
const databaseId = this.defaults.databaseId;
|
|
4050
|
+
if (!databaseId) {
|
|
4051
|
+
throw new Error("Notion databaseId is required to create work items.");
|
|
4052
|
+
}
|
|
4053
|
+
const titleProperty = this.defaults.titleProperty ?? "Name";
|
|
4054
|
+
const properties = {
|
|
4055
|
+
[titleProperty]: buildTitleProperty(input.title)
|
|
4056
|
+
};
|
|
4057
|
+
applySelect(properties, this.defaults.statusProperty, input.status);
|
|
4058
|
+
applySelect(properties, this.defaults.priorityProperty, input.priority);
|
|
4059
|
+
applyMultiSelect(properties, this.defaults.tagsProperty, input.tags);
|
|
4060
|
+
applyDate(properties, this.defaults.dueDateProperty, input.dueDate);
|
|
4061
|
+
applyRichText(properties, this.defaults.descriptionProperty, input.description);
|
|
4062
|
+
const page = await this.client.pages.create({
|
|
4063
|
+
parent: { type: "database_id", database_id: databaseId },
|
|
4064
|
+
properties
|
|
4065
|
+
});
|
|
4066
|
+
return {
|
|
4067
|
+
id: page.id,
|
|
4068
|
+
title: input.title,
|
|
4069
|
+
url: "url" in page ? page.url : undefined,
|
|
4070
|
+
status: input.status,
|
|
4071
|
+
priority: input.priority,
|
|
4072
|
+
tags: input.tags,
|
|
4073
|
+
projectId: databaseId,
|
|
4074
|
+
externalId: input.externalId,
|
|
4075
|
+
metadata: input.metadata
|
|
4076
|
+
};
|
|
4077
|
+
}
|
|
4078
|
+
async createWorkItems(items) {
|
|
4079
|
+
const created = [];
|
|
4080
|
+
for (const item of items) {
|
|
4081
|
+
created.push(await this.createWorkItem(item));
|
|
4082
|
+
}
|
|
4083
|
+
return created;
|
|
4084
|
+
}
|
|
4085
|
+
async createSummaryPage(input) {
|
|
4086
|
+
const parentId = this.defaults.summaryParentPageId;
|
|
4087
|
+
if (!parentId) {
|
|
4088
|
+
throw new Error("Notion summaryParentPageId is required for summaries.");
|
|
4089
|
+
}
|
|
4090
|
+
const summaryProperties = {
|
|
4091
|
+
title: buildTitleProperty(input.title)
|
|
4092
|
+
};
|
|
4093
|
+
const page = await this.client.pages.create({
|
|
4094
|
+
parent: { type: "page_id", page_id: parentId },
|
|
4095
|
+
properties: summaryProperties
|
|
4096
|
+
});
|
|
4097
|
+
if (input.description) {
|
|
4098
|
+
const children = buildParagraphBlocks(input.description);
|
|
4099
|
+
if (children.length > 0) {
|
|
4100
|
+
await this.client.blocks.children.append({
|
|
4101
|
+
block_id: page.id,
|
|
4102
|
+
children
|
|
4103
|
+
});
|
|
4104
|
+
}
|
|
4105
|
+
}
|
|
4106
|
+
return {
|
|
4107
|
+
id: page.id,
|
|
4108
|
+
title: input.title,
|
|
4109
|
+
url: "url" in page ? page.url : undefined,
|
|
4110
|
+
status: input.status,
|
|
4111
|
+
priority: input.priority,
|
|
4112
|
+
tags: input.tags,
|
|
4113
|
+
projectId: parentId,
|
|
4114
|
+
externalId: input.externalId,
|
|
4115
|
+
metadata: input.metadata
|
|
4116
|
+
};
|
|
4117
|
+
}
|
|
4118
|
+
}
|
|
4119
|
+
function buildTitleProperty(title) {
|
|
4120
|
+
return {
|
|
4121
|
+
title: [
|
|
4122
|
+
{
|
|
4123
|
+
type: "text",
|
|
4124
|
+
text: { content: title }
|
|
4125
|
+
}
|
|
4126
|
+
]
|
|
4127
|
+
};
|
|
4128
|
+
}
|
|
4129
|
+
function buildRichText(text) {
|
|
4130
|
+
return {
|
|
4131
|
+
rich_text: [
|
|
4132
|
+
{
|
|
4133
|
+
type: "text",
|
|
4134
|
+
text: { content: text }
|
|
4135
|
+
}
|
|
4136
|
+
]
|
|
4137
|
+
};
|
|
4138
|
+
}
|
|
4139
|
+
function applySelect(properties, property, value) {
|
|
4140
|
+
if (!property || !value)
|
|
4141
|
+
return;
|
|
4142
|
+
const next = {
|
|
4143
|
+
select: { name: value }
|
|
4144
|
+
};
|
|
4145
|
+
properties[property] = next;
|
|
4146
|
+
}
|
|
4147
|
+
function applyMultiSelect(properties, property, values) {
|
|
4148
|
+
if (!property || !values || values.length === 0)
|
|
4149
|
+
return;
|
|
4150
|
+
const next = {
|
|
4151
|
+
multi_select: values.map((value) => ({ name: value }))
|
|
4152
|
+
};
|
|
4153
|
+
properties[property] = next;
|
|
4154
|
+
}
|
|
4155
|
+
function applyDate(properties, property, value) {
|
|
4156
|
+
if (!property || !value)
|
|
4157
|
+
return;
|
|
4158
|
+
const next = {
|
|
4159
|
+
date: { start: value.toISOString() }
|
|
4160
|
+
};
|
|
4161
|
+
properties[property] = next;
|
|
4162
|
+
}
|
|
4163
|
+
function applyRichText(properties, property, value) {
|
|
4164
|
+
if (!property || !value)
|
|
4165
|
+
return;
|
|
4166
|
+
properties[property] = buildRichText(value);
|
|
4167
|
+
}
|
|
4168
|
+
function buildParagraphBlocks(text) {
|
|
4169
|
+
const lines = text.split(/\r?\n/).filter((line) => line.trim());
|
|
4170
|
+
return lines.map((line) => ({
|
|
4171
|
+
object: "block",
|
|
4172
|
+
type: "paragraph",
|
|
4173
|
+
paragraph: {
|
|
4174
|
+
rich_text: [
|
|
4175
|
+
{
|
|
4176
|
+
type: "text",
|
|
4177
|
+
text: { content: line }
|
|
4178
|
+
}
|
|
4179
|
+
]
|
|
4180
|
+
}
|
|
4181
|
+
}));
|
|
4182
|
+
}
|
|
4183
|
+
|
|
4184
|
+
// src/impls/tldv-meeting-recorder.ts
|
|
4185
|
+
var DEFAULT_BASE_URL4 = "https://pasta.tldv.io/v1alpha1";
|
|
4186
|
+
|
|
4187
|
+
class TldvMeetingRecorderProvider {
|
|
4188
|
+
apiKey;
|
|
4189
|
+
baseUrl;
|
|
4190
|
+
defaultPageSize;
|
|
4191
|
+
constructor(options) {
|
|
4192
|
+
this.apiKey = options.apiKey;
|
|
4193
|
+
this.baseUrl = options.baseUrl ?? DEFAULT_BASE_URL4;
|
|
4194
|
+
this.defaultPageSize = options.pageSize;
|
|
4195
|
+
}
|
|
4196
|
+
async listMeetings(params) {
|
|
4197
|
+
const page = params.cursor ? Number(params.cursor) : 1;
|
|
4198
|
+
const limit = params.pageSize ?? this.defaultPageSize ?? 50;
|
|
4199
|
+
const query = new URLSearchParams;
|
|
4200
|
+
query.set("page", String(Number.isFinite(page) ? page : 1));
|
|
4201
|
+
query.set("limit", String(limit));
|
|
4202
|
+
if (params.query)
|
|
4203
|
+
query.set("query", params.query);
|
|
4204
|
+
if (params.from)
|
|
4205
|
+
query.set("from", params.from);
|
|
4206
|
+
if (params.to)
|
|
4207
|
+
query.set("to", params.to);
|
|
4208
|
+
if (params.organizerEmail)
|
|
4209
|
+
query.set("organizer", params.organizerEmail);
|
|
4210
|
+
if (params.participantEmail)
|
|
4211
|
+
query.set("participant", params.participantEmail);
|
|
4212
|
+
const data = await this.request(`/meetings?${query.toString()}`);
|
|
4213
|
+
const nextPage = data.page < data.pages ? data.page + 1 : undefined;
|
|
4214
|
+
return {
|
|
4215
|
+
meetings: data.results.map((meeting) => this.mapMeeting(meeting, params)),
|
|
4216
|
+
nextCursor: nextPage ? String(nextPage) : undefined,
|
|
4217
|
+
hasMore: Boolean(nextPage)
|
|
4218
|
+
};
|
|
4219
|
+
}
|
|
4220
|
+
async getMeeting(params) {
|
|
4221
|
+
const meeting = await this.request(`/meetings/${encodeURIComponent(params.meetingId)}`);
|
|
4222
|
+
return this.mapMeeting(meeting, params);
|
|
4223
|
+
}
|
|
4224
|
+
async getTranscript(params) {
|
|
4225
|
+
const response = await this.request(`/meetings/${encodeURIComponent(params.meetingId)}/transcript`);
|
|
4226
|
+
const segments = response.data.map((segment, index) => this.mapTranscriptSegment(segment, index));
|
|
4227
|
+
return {
|
|
4228
|
+
id: response.id,
|
|
4229
|
+
meetingId: response.meetingId,
|
|
4230
|
+
tenantId: params.tenantId,
|
|
4231
|
+
connectionId: params.connectionId ?? "unknown",
|
|
4232
|
+
externalId: response.id,
|
|
4233
|
+
format: "segments",
|
|
4234
|
+
text: segments.map((segment) => segment.text).join(`
|
|
4235
|
+
`),
|
|
4236
|
+
segments,
|
|
4237
|
+
metadata: {
|
|
4238
|
+
providerMeetingId: response.meetingId
|
|
4239
|
+
},
|
|
4240
|
+
raw: response.data
|
|
4241
|
+
};
|
|
4242
|
+
}
|
|
4243
|
+
async parseWebhook(request) {
|
|
4244
|
+
const payload = request.parsedBody ?? JSON.parse(request.rawBody);
|
|
4245
|
+
const body = payload;
|
|
4246
|
+
return {
|
|
4247
|
+
providerKey: "meeting-recorder.tldv",
|
|
4248
|
+
eventType: body.event,
|
|
4249
|
+
meetingId: body.data?.id ?? body.data?.meetingId,
|
|
4250
|
+
receivedAt: body.executedAt,
|
|
4251
|
+
payload
|
|
4252
|
+
};
|
|
4253
|
+
}
|
|
4254
|
+
mapMeeting(meeting, params) {
|
|
4255
|
+
const connectionId = params.connectionId ?? "unknown";
|
|
4256
|
+
const invitees = meeting.invitees?.map((invitee) => this.mapInvitee(invitee)).filter(Boolean);
|
|
4257
|
+
return {
|
|
4258
|
+
id: meeting.id,
|
|
4259
|
+
tenantId: params.tenantId,
|
|
4260
|
+
connectionId,
|
|
4261
|
+
externalId: meeting.id,
|
|
4262
|
+
title: meeting.name,
|
|
4263
|
+
organizer: this.mapInvitee(meeting.organizer, "organizer"),
|
|
4264
|
+
invitees: invitees?.length ? invitees : undefined,
|
|
4265
|
+
participants: invitees?.length ? invitees : undefined,
|
|
4266
|
+
scheduledStartAt: meeting.happenedAt,
|
|
4267
|
+
recordingStartAt: meeting.happenedAt,
|
|
4268
|
+
durationSeconds: meeting.duration,
|
|
4269
|
+
meetingUrl: meeting.url,
|
|
4270
|
+
transcriptAvailable: true,
|
|
4271
|
+
sourcePlatform: "tldv",
|
|
4272
|
+
metadata: {
|
|
4273
|
+
template: meeting.template,
|
|
4274
|
+
extraProperties: meeting.extraProperties
|
|
4275
|
+
}
|
|
4276
|
+
};
|
|
4277
|
+
}
|
|
4278
|
+
mapInvitee(invitee, role = "attendee") {
|
|
4279
|
+
if (!invitee)
|
|
4280
|
+
return;
|
|
4281
|
+
return {
|
|
4282
|
+
name: invitee.name ?? undefined,
|
|
4283
|
+
email: invitee.email ?? undefined,
|
|
4284
|
+
role
|
|
4285
|
+
};
|
|
4286
|
+
}
|
|
4287
|
+
mapTranscriptSegment(segment, index) {
|
|
4288
|
+
return {
|
|
4289
|
+
index,
|
|
4290
|
+
speakerName: segment.speaker ?? undefined,
|
|
4291
|
+
text: segment.text,
|
|
4292
|
+
startTimeMs: parseSeconds2(segment.startTime),
|
|
4293
|
+
endTimeMs: parseSeconds2(segment.endTime)
|
|
4294
|
+
};
|
|
4295
|
+
}
|
|
4296
|
+
async request(path) {
|
|
4297
|
+
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
4298
|
+
headers: {
|
|
4299
|
+
"Content-Type": "application/json",
|
|
4300
|
+
"x-api-key": this.apiKey
|
|
4301
|
+
}
|
|
4302
|
+
});
|
|
4303
|
+
if (!response.ok) {
|
|
4304
|
+
const message = await safeReadError4(response);
|
|
4305
|
+
throw new Error(`tl;dv API error (${response.status}): ${message}`);
|
|
4306
|
+
}
|
|
4307
|
+
return await response.json();
|
|
4308
|
+
}
|
|
4309
|
+
}
|
|
4310
|
+
function parseSeconds2(value) {
|
|
4311
|
+
if (value == null)
|
|
4312
|
+
return;
|
|
4313
|
+
const num = typeof value === "number" ? value : Number(value);
|
|
4314
|
+
if (!Number.isFinite(num))
|
|
4315
|
+
return;
|
|
4316
|
+
return num * 1000;
|
|
4317
|
+
}
|
|
4318
|
+
async function safeReadError4(response) {
|
|
4319
|
+
try {
|
|
4320
|
+
const data = await response.json();
|
|
4321
|
+
return data?.message ?? response.statusText;
|
|
4322
|
+
} catch {
|
|
4323
|
+
return response.statusText;
|
|
4324
|
+
}
|
|
4325
|
+
}
|
|
4326
|
+
|
|
4327
|
+
// src/impls/provider-factory.ts
|
|
4328
|
+
import { Buffer as Buffer5 } from "node:buffer";
|
|
4329
|
+
var SECRET_CACHE = new Map;
|
|
4330
|
+
|
|
4331
|
+
class IntegrationProviderFactory {
|
|
4332
|
+
async createPaymentsProvider(context) {
|
|
4333
|
+
const secrets = await this.loadSecrets(context);
|
|
4334
|
+
switch (context.spec.meta.key) {
|
|
4335
|
+
case "payments.stripe":
|
|
4336
|
+
return new StripePaymentsProvider({
|
|
4337
|
+
apiKey: requireSecret(secrets, "apiKey", "Stripe API key is required")
|
|
4338
|
+
});
|
|
4339
|
+
default:
|
|
4340
|
+
throw new Error(`Unsupported payments integration: ${context.spec.meta.key}`);
|
|
4341
|
+
}
|
|
4342
|
+
}
|
|
4343
|
+
async createEmailOutboundProvider(context) {
|
|
4344
|
+
const secrets = await this.loadSecrets(context);
|
|
4345
|
+
switch (context.spec.meta.key) {
|
|
4346
|
+
case "email.postmark":
|
|
4347
|
+
return new PostmarkEmailProvider({
|
|
4348
|
+
serverToken: requireSecret(secrets, "serverToken", "Postmark server token is required"),
|
|
4349
|
+
defaultFromEmail: context.config.fromEmail,
|
|
4350
|
+
messageStream: context.config.messageStream
|
|
4351
|
+
});
|
|
4352
|
+
default:
|
|
4353
|
+
throw new Error(`Unsupported email integration: ${context.spec.meta.key}`);
|
|
4354
|
+
}
|
|
4355
|
+
}
|
|
4356
|
+
async createSmsProvider(context) {
|
|
4357
|
+
const secrets = await this.loadSecrets(context);
|
|
4358
|
+
switch (context.spec.meta.key) {
|
|
4359
|
+
case "sms.twilio":
|
|
4360
|
+
return new TwilioSmsProvider({
|
|
4361
|
+
accountSid: requireSecret(secrets, "accountSid", "Twilio account SID is required"),
|
|
4362
|
+
authToken: requireSecret(secrets, "authToken", "Twilio auth token is required"),
|
|
4363
|
+
fromNumber: context.config.fromNumber
|
|
4364
|
+
});
|
|
4365
|
+
default:
|
|
4366
|
+
throw new Error(`Unsupported SMS integration: ${context.spec.meta.key}`);
|
|
4367
|
+
}
|
|
4368
|
+
}
|
|
4369
|
+
async createVectorStoreProvider(context) {
|
|
4370
|
+
const secrets = await this.loadSecrets(context);
|
|
4371
|
+
const config = context.config;
|
|
4372
|
+
switch (context.spec.meta.key) {
|
|
4373
|
+
case "vectordb.qdrant":
|
|
4374
|
+
return new QdrantVectorProvider({
|
|
4375
|
+
url: requireConfig(context, "apiUrl", "Qdrant apiUrl config is required"),
|
|
4376
|
+
apiKey: secrets.apiKey
|
|
4377
|
+
});
|
|
4378
|
+
case "vectordb.supabase":
|
|
4379
|
+
return new SupabaseVectorProvider({
|
|
4380
|
+
connectionString: requireDatabaseUrl(secrets, "Supabase vector databaseUrl secret is required"),
|
|
4381
|
+
schema: config?.schema,
|
|
4382
|
+
table: config?.table,
|
|
4383
|
+
createTableIfMissing: config?.createTableIfMissing,
|
|
4384
|
+
distanceMetric: config?.distanceMetric,
|
|
4385
|
+
maxConnections: config?.maxConnections,
|
|
4386
|
+
sslMode: config?.sslMode
|
|
4387
|
+
});
|
|
4388
|
+
default:
|
|
4389
|
+
throw new Error(`Unsupported vector store integration: ${context.spec.meta.key}`);
|
|
4390
|
+
}
|
|
4391
|
+
}
|
|
4392
|
+
async createAnalyticsProvider(context) {
|
|
4393
|
+
const secrets = await this.loadSecrets(context);
|
|
4394
|
+
const config = context.config;
|
|
4395
|
+
switch (context.spec.meta.key) {
|
|
4396
|
+
case "analytics.posthog":
|
|
4397
|
+
return new PosthogAnalyticsProvider({
|
|
4398
|
+
host: config?.host,
|
|
4399
|
+
projectId: config?.projectId,
|
|
4400
|
+
mcpUrl: config?.mcpUrl,
|
|
4401
|
+
projectApiKey: secrets.projectApiKey,
|
|
4402
|
+
personalApiKey: requireSecret(secrets, "personalApiKey", "PostHog personalApiKey is required")
|
|
4403
|
+
});
|
|
4404
|
+
default:
|
|
4405
|
+
throw new Error(`Unsupported analytics integration: ${context.spec.meta.key}`);
|
|
4406
|
+
}
|
|
4407
|
+
}
|
|
4408
|
+
async createDatabaseProvider(context) {
|
|
4409
|
+
const secrets = await this.loadSecrets(context);
|
|
4410
|
+
const config = context.config;
|
|
4411
|
+
switch (context.spec.meta.key) {
|
|
4412
|
+
case "database.supabase":
|
|
4413
|
+
return new SupabasePostgresProvider({
|
|
4414
|
+
connectionString: requireDatabaseUrl(secrets, "Supabase database databaseUrl secret is required"),
|
|
4415
|
+
maxConnections: config?.maxConnections,
|
|
4416
|
+
sslMode: config?.sslMode
|
|
4417
|
+
});
|
|
4418
|
+
default:
|
|
4419
|
+
throw new Error(`Unsupported database integration: ${context.spec.meta.key}`);
|
|
4420
|
+
}
|
|
4421
|
+
}
|
|
4422
|
+
async createObjectStorageProvider(context) {
|
|
4423
|
+
const secrets = await this.loadSecrets(context);
|
|
4424
|
+
switch (context.spec.meta.key) {
|
|
4425
|
+
case "storage.s3":
|
|
4426
|
+
case "storage.gcs":
|
|
4427
|
+
return new GoogleCloudStorageProvider({
|
|
4428
|
+
bucket: requireConfig(context, "bucket", "Storage bucket is required"),
|
|
4429
|
+
clientOptions: secrets.type === "service_account" ? { credentials: secrets } : undefined
|
|
4430
|
+
});
|
|
4431
|
+
default:
|
|
4432
|
+
throw new Error(`Unsupported storage integration: ${context.spec.meta.key}`);
|
|
4433
|
+
}
|
|
4434
|
+
}
|
|
4435
|
+
async createVoiceProvider(context) {
|
|
4436
|
+
const secrets = await this.loadSecrets(context);
|
|
4437
|
+
const config = context.config;
|
|
4438
|
+
switch (context.spec.meta.key) {
|
|
4439
|
+
case "ai-voice.elevenlabs":
|
|
4440
|
+
return new ElevenLabsVoiceProvider({
|
|
4441
|
+
apiKey: requireSecret(secrets, "apiKey", "ElevenLabs API key is required"),
|
|
4442
|
+
defaultVoiceId: config?.defaultVoiceId
|
|
4443
|
+
});
|
|
4444
|
+
case "ai-voice.gradium":
|
|
4445
|
+
return new GradiumVoiceProvider({
|
|
4446
|
+
apiKey: requireSecret(secrets, "apiKey", "Gradium API key is required"),
|
|
4447
|
+
defaultVoiceId: config?.defaultVoiceId,
|
|
4448
|
+
region: config?.region,
|
|
4449
|
+
baseUrl: config?.baseUrl,
|
|
4450
|
+
timeoutMs: config?.timeoutMs,
|
|
4451
|
+
outputFormat: config?.outputFormat
|
|
4452
|
+
});
|
|
4453
|
+
case "ai-voice.fal":
|
|
4454
|
+
return new FalVoiceProvider({
|
|
4455
|
+
apiKey: requireSecret(secrets, "apiKey", "Fal API key is required"),
|
|
4456
|
+
modelId: config?.modelId,
|
|
4457
|
+
defaultVoiceUrl: config?.defaultVoiceUrl,
|
|
4458
|
+
defaultExaggeration: config?.defaultExaggeration,
|
|
4459
|
+
defaultTemperature: config?.defaultTemperature,
|
|
4460
|
+
defaultCfg: config?.defaultCfg,
|
|
4461
|
+
pollIntervalMs: config?.pollIntervalMs
|
|
4462
|
+
});
|
|
4463
|
+
default:
|
|
4464
|
+
throw new Error(`Unsupported voice integration: ${context.spec.meta.key}`);
|
|
4465
|
+
}
|
|
4466
|
+
}
|
|
4467
|
+
async createProjectManagementProvider(context) {
|
|
4468
|
+
const secrets = await this.loadSecrets(context);
|
|
4469
|
+
const config = context.config;
|
|
4470
|
+
switch (context.spec.meta.key) {
|
|
4471
|
+
case "project-management.linear":
|
|
4472
|
+
return new LinearProjectManagementProvider({
|
|
4473
|
+
apiKey: requireSecret(secrets, "apiKey", "Linear API key is required"),
|
|
4474
|
+
teamId: requireConfig(context, "teamId", "Linear teamId is required"),
|
|
4475
|
+
projectId: config?.projectId,
|
|
4476
|
+
assigneeId: config?.assigneeId,
|
|
4477
|
+
stateId: config?.stateId,
|
|
4478
|
+
labelIds: config?.labelIds,
|
|
4479
|
+
tagLabelMap: config?.tagLabelMap
|
|
4480
|
+
});
|
|
4481
|
+
case "project-management.jira":
|
|
4482
|
+
return new JiraProjectManagementProvider({
|
|
4483
|
+
siteUrl: requireConfig(context, "siteUrl", "Jira siteUrl is required"),
|
|
4484
|
+
email: requireSecret(secrets, "email", "Jira email is required"),
|
|
4485
|
+
apiToken: requireSecret(secrets, "apiToken", "Jira API token is required"),
|
|
4486
|
+
projectKey: config?.projectKey,
|
|
4487
|
+
issueType: config?.issueType,
|
|
4488
|
+
defaultLabels: config?.defaultLabels,
|
|
4489
|
+
issueTypeMap: config?.issueTypeMap
|
|
4490
|
+
});
|
|
4491
|
+
case "project-management.notion":
|
|
4492
|
+
return new NotionProjectManagementProvider({
|
|
4493
|
+
apiKey: requireSecret(secrets, "apiKey", "Notion API key is required"),
|
|
4494
|
+
databaseId: config?.databaseId,
|
|
4495
|
+
summaryParentPageId: config?.summaryParentPageId,
|
|
4496
|
+
titleProperty: config?.titleProperty,
|
|
4497
|
+
statusProperty: config?.statusProperty,
|
|
4498
|
+
priorityProperty: config?.priorityProperty,
|
|
4499
|
+
tagsProperty: config?.tagsProperty,
|
|
4500
|
+
dueDateProperty: config?.dueDateProperty,
|
|
4501
|
+
descriptionProperty: config?.descriptionProperty
|
|
4502
|
+
});
|
|
4503
|
+
default:
|
|
4504
|
+
throw new Error(`Unsupported project management integration: ${context.spec.meta.key}`);
|
|
4505
|
+
}
|
|
4506
|
+
}
|
|
4507
|
+
async createMeetingRecorderProvider(context) {
|
|
4508
|
+
const secrets = await this.loadSecrets(context);
|
|
4509
|
+
const config = context.config;
|
|
4510
|
+
switch (context.spec.meta.key) {
|
|
4511
|
+
case "meeting-recorder.granola":
|
|
4512
|
+
if (config?.transport === "mcp") {
|
|
4513
|
+
return new GranolaMeetingRecorderProvider({
|
|
4514
|
+
transport: "mcp",
|
|
4515
|
+
mcpUrl: config?.mcpUrl,
|
|
4516
|
+
mcpHeaders: config?.mcpHeaders,
|
|
4517
|
+
mcpAccessToken: secrets.mcpAccessToken ?? secrets.apiKey,
|
|
4518
|
+
pageSize: config?.pageSize
|
|
4519
|
+
});
|
|
4520
|
+
}
|
|
4521
|
+
return new GranolaMeetingRecorderProvider({
|
|
4522
|
+
apiKey: requireSecret(secrets, "apiKey", "Granola API key is required"),
|
|
4523
|
+
baseUrl: config?.baseUrl,
|
|
4524
|
+
pageSize: config?.pageSize
|
|
4525
|
+
});
|
|
4526
|
+
case "meeting-recorder.tldv":
|
|
4527
|
+
return new TldvMeetingRecorderProvider({
|
|
4528
|
+
apiKey: requireSecret(secrets, "apiKey", "tl;dv API key is required"),
|
|
4529
|
+
baseUrl: config?.baseUrl,
|
|
4530
|
+
pageSize: config?.pageSize
|
|
4531
|
+
});
|
|
4532
|
+
case "meeting-recorder.fireflies":
|
|
4533
|
+
return new FirefliesMeetingRecorderProvider({
|
|
4534
|
+
apiKey: requireSecret(secrets, "apiKey", "Fireflies API key is required"),
|
|
4535
|
+
baseUrl: config?.baseUrl,
|
|
4536
|
+
pageSize: config?.transcriptsPageSize ?? config?.pageSize,
|
|
4537
|
+
webhookSecret: secrets.webhookSecret
|
|
4538
|
+
});
|
|
4539
|
+
case "meeting-recorder.fathom":
|
|
4540
|
+
return new FathomMeetingRecorderProvider({
|
|
4541
|
+
apiKey: requireSecret(secrets, "apiKey", "Fathom API key is required"),
|
|
4542
|
+
baseUrl: config?.baseUrl,
|
|
4543
|
+
includeTranscript: config?.includeTranscript,
|
|
4544
|
+
includeSummary: config?.includeSummary,
|
|
4545
|
+
includeActionItems: config?.includeActionItems,
|
|
4546
|
+
includeCrmMatches: config?.includeCrmMatches,
|
|
4547
|
+
triggeredFor: config?.triggeredFor,
|
|
4548
|
+
maxPages: config?.maxPages,
|
|
4549
|
+
webhookSecret: secrets.webhookSecret
|
|
4550
|
+
});
|
|
4551
|
+
default:
|
|
4552
|
+
throw new Error(`Unsupported meeting recorder integration: ${context.spec.meta.key}`);
|
|
4553
|
+
}
|
|
4554
|
+
}
|
|
4555
|
+
async createLlmProvider(context) {
|
|
4556
|
+
const secrets = await this.loadSecrets(context);
|
|
4557
|
+
switch (context.spec.meta.key) {
|
|
4558
|
+
case "ai-llm.mistral":
|
|
4559
|
+
return new MistralLLMProvider({
|
|
4560
|
+
apiKey: requireSecret(secrets, "apiKey", "Mistral API key is required"),
|
|
4561
|
+
defaultModel: context.config.model
|
|
4562
|
+
});
|
|
4563
|
+
default:
|
|
4564
|
+
throw new Error(`Unsupported LLM integration: ${context.spec.meta.key}`);
|
|
4565
|
+
}
|
|
4566
|
+
}
|
|
4567
|
+
async createEmbeddingProvider(context) {
|
|
4568
|
+
const secrets = await this.loadSecrets(context);
|
|
4569
|
+
switch (context.spec.meta.key) {
|
|
4570
|
+
case "ai-llm.mistral":
|
|
4571
|
+
return new MistralEmbeddingProvider({
|
|
4572
|
+
apiKey: requireSecret(secrets, "apiKey", "Mistral API key is required"),
|
|
4573
|
+
defaultModel: context.config.embeddingModel
|
|
4574
|
+
});
|
|
4575
|
+
default:
|
|
4576
|
+
throw new Error(`Unsupported embeddings integration: ${context.spec.meta.key}`);
|
|
4577
|
+
}
|
|
4578
|
+
}
|
|
4579
|
+
async createOpenBankingProvider(context) {
|
|
4580
|
+
const secrets = await this.loadSecrets(context);
|
|
4581
|
+
const config = context.config;
|
|
4582
|
+
switch (context.spec.meta.key) {
|
|
4583
|
+
case "openbanking.powens": {
|
|
4584
|
+
const environmentValue = requireConfig(context, "environment", "Powens environment (sandbox | production) must be specified in integration config.");
|
|
4585
|
+
if (environmentValue !== "sandbox" && environmentValue !== "production") {
|
|
4586
|
+
throw new Error(`Powens environment "${environmentValue}" is invalid. Expected "sandbox" or "production".`);
|
|
4587
|
+
}
|
|
4588
|
+
return new PowensOpenBankingProvider({
|
|
4589
|
+
clientId: requireSecret(secrets, "clientId", "Powens clientId is required"),
|
|
4590
|
+
clientSecret: requireSecret(secrets, "clientSecret", "Powens clientSecret is required"),
|
|
4591
|
+
apiKey: secrets.apiKey,
|
|
4592
|
+
environment: environmentValue,
|
|
4593
|
+
baseUrl: config?.baseUrl
|
|
4594
|
+
});
|
|
4595
|
+
}
|
|
4596
|
+
default:
|
|
4597
|
+
throw new Error(`Unsupported open banking integration: ${context.spec.meta.key}`);
|
|
4598
|
+
}
|
|
4599
|
+
}
|
|
4600
|
+
async loadSecrets(context) {
|
|
4601
|
+
const cacheKey = context.connection.meta.id;
|
|
4602
|
+
if (SECRET_CACHE.has(cacheKey)) {
|
|
4603
|
+
const cached = SECRET_CACHE.get(cacheKey);
|
|
4604
|
+
return cached ?? {};
|
|
4605
|
+
}
|
|
4606
|
+
const secret = await context.secretProvider.getSecret(context.secretReference);
|
|
4607
|
+
const value = parseSecret(secret);
|
|
4608
|
+
SECRET_CACHE.set(cacheKey, value);
|
|
4609
|
+
return value;
|
|
4610
|
+
}
|
|
4611
|
+
}
|
|
4612
|
+
function parseSecret(secret) {
|
|
4613
|
+
const text = Buffer5.from(secret.data).toString("utf-8").trim();
|
|
4614
|
+
if (!text)
|
|
4615
|
+
return {};
|
|
4616
|
+
try {
|
|
4617
|
+
return JSON.parse(text);
|
|
4618
|
+
} catch {
|
|
4619
|
+
return { apiKey: text };
|
|
4620
|
+
}
|
|
4621
|
+
}
|
|
4622
|
+
function requireSecret(secrets, key, message) {
|
|
4623
|
+
const value = secrets[key];
|
|
4624
|
+
if (value == null || value === "") {
|
|
4625
|
+
throw new Error(message);
|
|
4626
|
+
}
|
|
4627
|
+
return value;
|
|
4628
|
+
}
|
|
4629
|
+
function requireDatabaseUrl(secrets, message) {
|
|
4630
|
+
const value = secrets.databaseUrl ?? secrets.connectionString ?? secrets.postgresUrl ?? secrets.apiKey;
|
|
4631
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
4632
|
+
throw new Error(message);
|
|
4633
|
+
}
|
|
4634
|
+
return value;
|
|
4635
|
+
}
|
|
4636
|
+
function requireConfig(context, key, message) {
|
|
4637
|
+
const config = context.config;
|
|
4638
|
+
const value = config?.[key];
|
|
4639
|
+
if (value == null) {
|
|
4640
|
+
throw new Error(message);
|
|
4641
|
+
}
|
|
4642
|
+
return value;
|
|
4643
|
+
}
|
|
4644
|
+
// src/openbanking.ts
|
|
4645
|
+
export * from "@contractspec/lib.contracts/integrations/providers/openbanking";
|
|
4646
|
+
|
|
4647
|
+
// src/llm.ts
|
|
4648
|
+
export * from "@contractspec/lib.contracts/integrations/providers/llm";
|
|
4649
|
+
|
|
4650
|
+
// src/vector-store.ts
|
|
4651
|
+
export * from "@contractspec/lib.contracts/integrations/providers/vector-store";
|
|
4652
|
+
|
|
4653
|
+
// src/storage.ts
|
|
4654
|
+
export * from "@contractspec/lib.contracts/integrations/providers/storage";
|
|
4655
|
+
|
|
4656
|
+
// src/sms.ts
|
|
4657
|
+
export * from "@contractspec/lib.contracts/integrations/providers/sms";
|
|
4658
|
+
|
|
4659
|
+
// src/payments.ts
|
|
4660
|
+
export * from "@contractspec/lib.contracts/integrations/providers/payments";
|
|
4661
|
+
|
|
4662
|
+
// src/voice.ts
|
|
4663
|
+
export * from "@contractspec/lib.contracts/integrations/providers/voice";
|
|
4664
|
+
|
|
4665
|
+
// src/project-management.ts
|
|
4666
|
+
export * from "@contractspec/lib.contracts/integrations/providers/project-management";
|
|
4667
|
+
|
|
4668
|
+
// src/meeting-recorder.ts
|
|
4669
|
+
export * from "@contractspec/lib.contracts/integrations/providers/meeting-recorder";
|
|
4670
|
+
export {
|
|
4671
|
+
TwilioSmsProvider,
|
|
4672
|
+
TldvMeetingRecorderProvider,
|
|
4673
|
+
SupabaseVectorProvider,
|
|
4674
|
+
SupabasePostgresProvider,
|
|
4675
|
+
StripePaymentsProvider,
|
|
4676
|
+
QdrantVectorProvider,
|
|
4677
|
+
PowensOpenBankingProvider,
|
|
4678
|
+
PowensClientError,
|
|
4679
|
+
PowensClient,
|
|
4680
|
+
PostmarkEmailProvider,
|
|
4681
|
+
PosthogAnalyticsReader,
|
|
4682
|
+
PosthogAnalyticsProvider,
|
|
4683
|
+
NotionProjectManagementProvider,
|
|
4684
|
+
MistralLLMProvider,
|
|
4685
|
+
MistralEmbeddingProvider,
|
|
4686
|
+
LinearProjectManagementProvider,
|
|
4687
|
+
JiraProjectManagementProvider,
|
|
4688
|
+
IntegrationProviderFactory,
|
|
4689
|
+
GranolaMeetingRecorderProvider,
|
|
4690
|
+
GradiumVoiceProvider,
|
|
4691
|
+
GoogleCloudStorageProvider,
|
|
4692
|
+
GoogleCalendarProvider,
|
|
4693
|
+
GmailOutboundProvider,
|
|
4694
|
+
GmailInboundProvider,
|
|
4695
|
+
FirefliesMeetingRecorderProvider,
|
|
4696
|
+
FathomMeetingRecorderProvider,
|
|
4697
|
+
FalVoiceProvider,
|
|
4698
|
+
ElevenLabsVoiceProvider
|
|
4699
|
+
};
|