fireflies-api 0.5.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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +64 -0
  3. package/dist/action-items-CC9yUxHY.d.cts +380 -0
  4. package/dist/action-items-CC9yUxHY.d.ts +380 -0
  5. package/dist/cli/index.cjs +3909 -0
  6. package/dist/cli/index.cjs.map +1 -0
  7. package/dist/cli/index.d.cts +2 -0
  8. package/dist/cli/index.d.ts +2 -0
  9. package/dist/cli/index.js +3906 -0
  10. package/dist/cli/index.js.map +1 -0
  11. package/dist/index.cjs +3389 -0
  12. package/dist/index.cjs.map +1 -0
  13. package/dist/index.d.cts +966 -0
  14. package/dist/index.d.ts +966 -0
  15. package/dist/index.js +3344 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/middleware/express.cjs +2491 -0
  18. package/dist/middleware/express.cjs.map +1 -0
  19. package/dist/middleware/express.d.cts +36 -0
  20. package/dist/middleware/express.d.ts +36 -0
  21. package/dist/middleware/express.js +2489 -0
  22. package/dist/middleware/express.js.map +1 -0
  23. package/dist/middleware/fastify.cjs +2501 -0
  24. package/dist/middleware/fastify.cjs.map +1 -0
  25. package/dist/middleware/fastify.d.cts +66 -0
  26. package/dist/middleware/fastify.d.ts +66 -0
  27. package/dist/middleware/fastify.js +2498 -0
  28. package/dist/middleware/fastify.js.map +1 -0
  29. package/dist/middleware/hono.cjs +2493 -0
  30. package/dist/middleware/hono.cjs.map +1 -0
  31. package/dist/middleware/hono.d.cts +37 -0
  32. package/dist/middleware/hono.d.ts +37 -0
  33. package/dist/middleware/hono.js +2490 -0
  34. package/dist/middleware/hono.js.map +1 -0
  35. package/dist/schemas/index.cjs +307 -0
  36. package/dist/schemas/index.cjs.map +1 -0
  37. package/dist/schemas/index.d.cts +926 -0
  38. package/dist/schemas/index.d.ts +926 -0
  39. package/dist/schemas/index.js +268 -0
  40. package/dist/schemas/index.js.map +1 -0
  41. package/dist/speaker-analytics-Dr46LKyP.d.ts +275 -0
  42. package/dist/speaker-analytics-l45LXqO1.d.cts +275 -0
  43. package/dist/types-BX-3JcRI.d.cts +41 -0
  44. package/dist/types-C_XxdRd1.d.cts +1546 -0
  45. package/dist/types-CaHcwnKw.d.ts +41 -0
  46. package/dist/types-DIPZmUl3.d.ts +1546 -0
  47. package/package.json +126 -0
@@ -0,0 +1,2501 @@
1
+ 'use strict';
2
+
3
+ var socket_ioClient = require('socket.io-client');
4
+ var crypto = require('crypto');
5
+
6
+ // src/errors.ts
7
+ var FirefliesError = class extends Error {
8
+ code = "FIREFLIES_ERROR";
9
+ status;
10
+ constructor(message, options) {
11
+ super(message, { cause: options?.cause });
12
+ this.name = "FirefliesError";
13
+ this.status = options?.status;
14
+ }
15
+ };
16
+ var AuthenticationError = class extends FirefliesError {
17
+ code = "AUTHENTICATION_ERROR";
18
+ constructor(message = "Invalid or missing API key") {
19
+ super(message, { status: 401 });
20
+ this.name = "AuthenticationError";
21
+ }
22
+ };
23
+ var RateLimitError = class extends FirefliesError {
24
+ code = "RATE_LIMIT_ERROR";
25
+ /** Suggested wait time in milliseconds before retrying. */
26
+ retryAfter;
27
+ constructor(message = "Rate limit exceeded", retryAfter) {
28
+ super(message, { status: 429 });
29
+ this.name = "RateLimitError";
30
+ this.retryAfter = retryAfter;
31
+ }
32
+ };
33
+ var NotFoundError = class extends FirefliesError {
34
+ code = "NOT_FOUND";
35
+ constructor(message = "Resource not found") {
36
+ super(message, { status: 404 });
37
+ this.name = "NotFoundError";
38
+ }
39
+ };
40
+ var ValidationError = class extends FirefliesError {
41
+ code = "VALIDATION_ERROR";
42
+ constructor(message) {
43
+ super(message, { status: 400 });
44
+ this.name = "ValidationError";
45
+ }
46
+ };
47
+ var GraphQLError = class extends FirefliesError {
48
+ code = "GRAPHQL_ERROR";
49
+ errors;
50
+ constructor(message, errors) {
51
+ super(message);
52
+ this.name = "GraphQLError";
53
+ this.errors = errors;
54
+ }
55
+ };
56
+ var TimeoutError = class extends FirefliesError {
57
+ code = "TIMEOUT_ERROR";
58
+ constructor(message = "Request timed out") {
59
+ super(message, { status: 408 });
60
+ this.name = "TimeoutError";
61
+ }
62
+ };
63
+ var NetworkError = class extends FirefliesError {
64
+ code = "NETWORK_ERROR";
65
+ constructor(message, cause) {
66
+ super(message, { cause });
67
+ this.name = "NetworkError";
68
+ }
69
+ };
70
+ var RealtimeError = class extends FirefliesError {
71
+ code = "REALTIME_ERROR";
72
+ constructor(message, options) {
73
+ super(message, options);
74
+ this.name = "RealtimeError";
75
+ }
76
+ };
77
+ var ConnectionError = class extends RealtimeError {
78
+ code = "CONNECTION_ERROR";
79
+ constructor(message = "Failed to establish realtime connection", options) {
80
+ super(message, options);
81
+ this.name = "ConnectionError";
82
+ }
83
+ };
84
+ var StreamClosedError = class extends RealtimeError {
85
+ code = "STREAM_CLOSED";
86
+ constructor(message = "Stream has been closed") {
87
+ super(message);
88
+ this.name = "StreamClosedError";
89
+ }
90
+ };
91
+ var WebhookVerificationError = class extends FirefliesError {
92
+ code = "WEBHOOK_VERIFICATION_FAILED";
93
+ constructor(message) {
94
+ super(message, { status: 401 });
95
+ this.name = "WebhookVerificationError";
96
+ }
97
+ };
98
+ var WebhookParseError = class extends FirefliesError {
99
+ code = "WEBHOOK_PARSE_FAILED";
100
+ constructor(message) {
101
+ super(message, { status: 400 });
102
+ this.name = "WebhookParseError";
103
+ }
104
+ };
105
+ function parseErrorResponse(status, body, defaultMessage) {
106
+ const message = extractErrorMessage(body) ?? defaultMessage;
107
+ switch (status) {
108
+ case 401:
109
+ return new AuthenticationError(message);
110
+ case 404:
111
+ return new NotFoundError(message);
112
+ case 429: {
113
+ const retryAfter = extractRetryAfter(body);
114
+ return new RateLimitError(message, retryAfter);
115
+ }
116
+ case 400:
117
+ return new ValidationError(message);
118
+ default:
119
+ return new FirefliesError(message, { status });
120
+ }
121
+ }
122
+ function extractErrorMessage(body) {
123
+ if (typeof body === "object" && body !== null) {
124
+ const obj = body;
125
+ if (typeof obj.message === "string") {
126
+ return obj.message;
127
+ }
128
+ if (typeof obj.error === "string") {
129
+ return obj.error;
130
+ }
131
+ }
132
+ return void 0;
133
+ }
134
+ function extractRetryAfter(body) {
135
+ if (typeof body === "object" && body !== null) {
136
+ const obj = body;
137
+ if (typeof obj.retryAfter === "number") {
138
+ return obj.retryAfter;
139
+ }
140
+ }
141
+ return void 0;
142
+ }
143
+
144
+ // src/utils/rate-limit-tracker.ts
145
+ var RATE_LIMIT_REMAINING_HEADER = "x-ratelimit-remaining-api";
146
+ var RATE_LIMIT_LIMIT_HEADER = "x-ratelimit-limit-api";
147
+ var RATE_LIMIT_RESET_HEADER = "x-ratelimit-reset-api";
148
+ var RateLimitTracker = class {
149
+ _remaining;
150
+ _limit;
151
+ _resetInSeconds;
152
+ _updatedAt;
153
+ warningThreshold;
154
+ /**
155
+ * Create a new RateLimitTracker.
156
+ * @param warningThreshold - Threshold below which isLow returns true
157
+ */
158
+ constructor(warningThreshold = 10) {
159
+ this._remaining = void 0;
160
+ this._limit = void 0;
161
+ this._resetInSeconds = void 0;
162
+ this._updatedAt = 0;
163
+ this.warningThreshold = warningThreshold;
164
+ }
165
+ /**
166
+ * Get the current rate limit state.
167
+ */
168
+ get state() {
169
+ return {
170
+ remaining: this._remaining,
171
+ limit: this._limit,
172
+ resetInSeconds: this._resetInSeconds,
173
+ updatedAt: this._updatedAt
174
+ };
175
+ }
176
+ /**
177
+ * Check if remaining requests are below the warning threshold.
178
+ * Returns false if remaining is undefined (header not received).
179
+ */
180
+ get isLow() {
181
+ return this._remaining !== void 0 && this._remaining < this.warningThreshold;
182
+ }
183
+ /**
184
+ * Update state from response headers.
185
+ * Extracts x-ratelimit-remaining-api, x-ratelimit-limit-api, and x-ratelimit-reset-api headers.
186
+ *
187
+ * @param headers - Response headers (Headers object or plain object)
188
+ */
189
+ update(headers) {
190
+ const remaining = this.getHeader(headers, RATE_LIMIT_REMAINING_HEADER);
191
+ if (remaining !== null) {
192
+ const parsed = Number.parseInt(remaining, 10);
193
+ if (!Number.isNaN(parsed) && parsed >= 0) {
194
+ this._remaining = parsed;
195
+ }
196
+ }
197
+ const limit = this.getHeader(headers, RATE_LIMIT_LIMIT_HEADER);
198
+ if (limit !== null) {
199
+ const parsed = Number.parseInt(limit, 10);
200
+ if (!Number.isNaN(parsed) && parsed >= 0) {
201
+ this._limit = parsed;
202
+ }
203
+ }
204
+ const reset = this.getHeader(headers, RATE_LIMIT_RESET_HEADER);
205
+ if (reset !== null) {
206
+ const parsed = Number.parseInt(reset, 10);
207
+ if (!Number.isNaN(parsed) && parsed >= 0) {
208
+ this._resetInSeconds = parsed;
209
+ }
210
+ }
211
+ this._updatedAt = Date.now();
212
+ }
213
+ /**
214
+ * Reset the tracker to initial state.
215
+ */
216
+ reset() {
217
+ this._remaining = void 0;
218
+ this._limit = void 0;
219
+ this._resetInSeconds = void 0;
220
+ this._updatedAt = 0;
221
+ }
222
+ /**
223
+ * Calculate the delay to apply before the next request.
224
+ * Returns 0 if throttling is disabled or not needed.
225
+ *
226
+ * Uses linear interpolation: more delay as remaining approaches 0.
227
+ * - remaining >= startThreshold: no delay
228
+ * - remaining = 0: maxDelay
229
+ * - remaining in between: proportional delay
230
+ *
231
+ * @param config - Throttle configuration
232
+ * @returns Delay in milliseconds
233
+ */
234
+ getThrottleDelay(config) {
235
+ if (!config?.enabled) {
236
+ return 0;
237
+ }
238
+ if (this._remaining === void 0) {
239
+ return 0;
240
+ }
241
+ const startThreshold = Math.max(1, config.startThreshold ?? 20);
242
+ let minDelay = config.minDelay ?? 100;
243
+ let maxDelay = config.maxDelay ?? 2e3;
244
+ if (minDelay > maxDelay) {
245
+ [minDelay, maxDelay] = [maxDelay, minDelay];
246
+ }
247
+ if (this._remaining >= startThreshold) {
248
+ return 0;
249
+ }
250
+ if (this._remaining <= 0) {
251
+ return maxDelay;
252
+ }
253
+ const ratio = 1 - this._remaining / startThreshold;
254
+ return Math.round(minDelay + ratio * (maxDelay - minDelay));
255
+ }
256
+ /**
257
+ * Extract a header value from Headers object or plain object.
258
+ * For plain objects, performs case-insensitive key lookup.
259
+ */
260
+ getHeader(headers, name) {
261
+ if (headers instanceof Headers) {
262
+ return headers.get(name);
263
+ }
264
+ const lowerName = name.toLowerCase();
265
+ for (const key of Object.keys(headers)) {
266
+ if (key.toLowerCase() === lowerName) {
267
+ return headers[key] ?? null;
268
+ }
269
+ }
270
+ return null;
271
+ }
272
+ };
273
+
274
+ // src/utils/retry.ts
275
+ var DEFAULT_OPTIONS = {
276
+ maxRetries: 3,
277
+ baseDelay: 1e3,
278
+ maxDelay: 3e4
279
+ };
280
+ async function retry(fn, options) {
281
+ const maxRetries = options?.maxRetries ?? DEFAULT_OPTIONS.maxRetries;
282
+ const baseDelay = options?.baseDelay ?? DEFAULT_OPTIONS.baseDelay;
283
+ const maxDelay = options?.maxDelay ?? DEFAULT_OPTIONS.maxDelay;
284
+ const shouldRetry = options?.shouldRetry ?? isRetryableError;
285
+ let lastError;
286
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
287
+ try {
288
+ return await fn();
289
+ } catch (error) {
290
+ lastError = error;
291
+ if (attempt >= maxRetries || !shouldRetry(error, attempt)) {
292
+ throw error;
293
+ }
294
+ const delay = calculateDelay(error, attempt, baseDelay, maxDelay);
295
+ await sleep(delay);
296
+ }
297
+ }
298
+ throw lastError;
299
+ }
300
+ function isRetryableError(error) {
301
+ if (error instanceof RateLimitError) {
302
+ return true;
303
+ }
304
+ if (error instanceof TimeoutError) {
305
+ return true;
306
+ }
307
+ if (error instanceof NetworkError) {
308
+ return true;
309
+ }
310
+ if (error instanceof Error && "status" in error && typeof error.status === "number") {
311
+ return error.status >= 500 && error.status < 600;
312
+ }
313
+ return false;
314
+ }
315
+ function calculateDelay(error, attempt, baseDelay, maxDelay) {
316
+ if (error instanceof RateLimitError && error.retryAfter !== void 0) {
317
+ return Math.min(error.retryAfter, maxDelay);
318
+ }
319
+ const exponentialDelay = baseDelay * 2 ** attempt;
320
+ const jitter = Math.random() * 0.1 * exponentialDelay;
321
+ return Math.min(exponentialDelay + jitter, maxDelay);
322
+ }
323
+ function sleep(ms) {
324
+ return new Promise((resolve) => setTimeout(resolve, ms));
325
+ }
326
+
327
+ // src/graphql/client.ts
328
+ var DEFAULT_BASE_URL = "https://api.fireflies.ai/graphql";
329
+ var DEFAULT_TIMEOUT = 3e4;
330
+ var GraphQLClient = class {
331
+ apiKey;
332
+ baseUrl;
333
+ timeout;
334
+ retryOptions;
335
+ rateLimitTracker;
336
+ rateLimitConfig;
337
+ lastWarningRemaining;
338
+ constructor(config) {
339
+ if (!config.apiKey) {
340
+ throw new FirefliesError("API key is required");
341
+ }
342
+ this.apiKey = config.apiKey;
343
+ this.baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
344
+ this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
345
+ this.retryOptions = buildRetryOptions(config.retry);
346
+ if (config.rateLimit) {
347
+ const warningThreshold = config.rateLimit.warningThreshold ?? 10;
348
+ this.rateLimitTracker = new RateLimitTracker(warningThreshold);
349
+ this.rateLimitConfig = config.rateLimit;
350
+ } else {
351
+ this.rateLimitTracker = null;
352
+ this.rateLimitConfig = null;
353
+ }
354
+ }
355
+ /**
356
+ * Get the current rate limit state.
357
+ * Returns undefined if rate limit tracking is not configured.
358
+ */
359
+ get rateLimitState() {
360
+ return this.rateLimitTracker?.state;
361
+ }
362
+ /**
363
+ * Execute a GraphQL query or mutation.
364
+ *
365
+ * @param query - GraphQL query string
366
+ * @param variables - Optional query variables
367
+ * @returns The data from the GraphQL response
368
+ * @throws GraphQLError if the response contains errors
369
+ * @throws AuthenticationError if the API key is invalid
370
+ * @throws RateLimitError if rate limits are exceeded
371
+ */
372
+ async execute(query, variables) {
373
+ return retry(() => this.executeOnce(query, variables), this.retryOptions);
374
+ }
375
+ async executeOnce(query, variables) {
376
+ await this.applyThrottleDelay();
377
+ const controller = new AbortController();
378
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
379
+ try {
380
+ const response = await fetch(this.baseUrl, {
381
+ method: "POST",
382
+ headers: {
383
+ "Content-Type": "application/json",
384
+ Authorization: `Bearer ${this.apiKey}`
385
+ },
386
+ body: JSON.stringify({ query, variables }),
387
+ signal: controller.signal
388
+ });
389
+ clearTimeout(timeoutId);
390
+ this.updateRateLimitState(response.headers);
391
+ if (!response.ok) {
392
+ const body = await this.safeParseJson(response);
393
+ if (response.status === 429) {
394
+ const retryAfter = this.parseRetryAfter(response.headers);
395
+ this.invokeRateLimitedCallback(retryAfter);
396
+ throw parseErrorResponse(
397
+ response.status,
398
+ body,
399
+ `GraphQL request failed with status ${response.status}`
400
+ );
401
+ }
402
+ throw parseErrorResponse(
403
+ response.status,
404
+ body,
405
+ `GraphQL request failed with status ${response.status}`
406
+ );
407
+ }
408
+ const json = await response.json();
409
+ if (json.errors && json.errors.length > 0) {
410
+ throw this.parseGraphQLErrors(json.errors);
411
+ }
412
+ if (json.data === void 0) {
413
+ throw new FirefliesError("GraphQL response missing data field");
414
+ }
415
+ return json.data;
416
+ } catch (error) {
417
+ clearTimeout(timeoutId);
418
+ if (error instanceof FirefliesError) {
419
+ throw error;
420
+ }
421
+ if (error instanceof Error) {
422
+ if (error.name === "AbortError") {
423
+ throw new TimeoutError(`Request timed out after ${this.timeout}ms`);
424
+ }
425
+ throw new NetworkError(`Network request failed: ${error.message}`, error);
426
+ }
427
+ throw new NetworkError("Unknown network error occurred", error);
428
+ }
429
+ }
430
+ async safeParseJson(response) {
431
+ try {
432
+ return await response.json();
433
+ } catch {
434
+ return null;
435
+ }
436
+ }
437
+ /**
438
+ * Apply throttle delay before request if configured.
439
+ */
440
+ async applyThrottleDelay() {
441
+ if (!this.rateLimitTracker || !this.rateLimitConfig?.throttle) {
442
+ return;
443
+ }
444
+ const delay = this.rateLimitTracker.getThrottleDelay(this.rateLimitConfig.throttle);
445
+ if (delay > 0) {
446
+ await sleep2(delay);
447
+ }
448
+ }
449
+ /**
450
+ * Update rate limit state from response headers and invoke callbacks.
451
+ */
452
+ updateRateLimitState(headers) {
453
+ if (!this.rateLimitTracker || !this.rateLimitConfig) {
454
+ return;
455
+ }
456
+ const wasLow = this.rateLimitTracker.isLow;
457
+ this.rateLimitTracker.update(headers);
458
+ const state = this.rateLimitTracker.state;
459
+ this.safeCallback(() => this.rateLimitConfig?.onUpdate?.(state));
460
+ if (this.rateLimitTracker.isLow) {
461
+ const shouldWarn = !wasLow || // Just crossed threshold
462
+ state.remaining !== void 0 && this.lastWarningRemaining !== void 0 && state.remaining < this.lastWarningRemaining;
463
+ if (shouldWarn) {
464
+ this.lastWarningRemaining = state.remaining;
465
+ this.safeCallback(() => this.rateLimitConfig?.onWarning?.(state));
466
+ }
467
+ }
468
+ }
469
+ /**
470
+ * Parse Retry-After header value.
471
+ */
472
+ parseRetryAfter(headers) {
473
+ const value = headers.get("retry-after");
474
+ if (!value) return void 0;
475
+ const parsed = Number.parseInt(value, 10);
476
+ return Number.isNaN(parsed) ? void 0 : parsed;
477
+ }
478
+ /**
479
+ * Invoke the onRateLimited callback.
480
+ */
481
+ invokeRateLimitedCallback(retryAfter) {
482
+ if (!this.rateLimitTracker || !this.rateLimitConfig?.onRateLimited) {
483
+ return;
484
+ }
485
+ const state = this.rateLimitTracker.state;
486
+ this.safeCallback(() => this.rateLimitConfig?.onRateLimited?.(state, retryAfter));
487
+ }
488
+ /**
489
+ * Safely invoke a callback, catching any errors to prevent user code from breaking the SDK.
490
+ */
491
+ safeCallback(fn) {
492
+ try {
493
+ fn();
494
+ } catch {
495
+ }
496
+ }
497
+ parseGraphQLErrors(errors) {
498
+ const firstError = errors[0];
499
+ if (!firstError) {
500
+ return new GraphQLError("Unknown GraphQL error", errors);
501
+ }
502
+ const message = firstError.message;
503
+ if (message.toLowerCase().includes("unauthorized") || message.toLowerCase().includes("authentication")) {
504
+ return parseErrorResponse(401, { message }, message);
505
+ }
506
+ if (message.toLowerCase().includes("not found")) {
507
+ return parseErrorResponse(404, { message }, message);
508
+ }
509
+ return new GraphQLError(message, errors);
510
+ }
511
+ };
512
+ function buildRetryOptions(config) {
513
+ if (!config) {
514
+ return {};
515
+ }
516
+ return {
517
+ maxRetries: config.maxRetries,
518
+ baseDelay: config.baseDelay,
519
+ maxDelay: config.maxDelay
520
+ };
521
+ }
522
+ function sleep2(ms) {
523
+ return new Promise((resolve) => setTimeout(resolve, ms));
524
+ }
525
+
526
+ // src/graphql/mutations/audio.ts
527
+ function createAudioAPI(client) {
528
+ return {
529
+ async upload(params) {
530
+ const mutation = `
531
+ mutation UploadAudio($input: AudioUploadInput!) {
532
+ uploadAudio(input: $input) {
533
+ success
534
+ title
535
+ message
536
+ }
537
+ }
538
+ `;
539
+ const data = await client.execute(mutation, {
540
+ input: params
541
+ });
542
+ return data.uploadAudio;
543
+ }
544
+ };
545
+ }
546
+
547
+ // src/graphql/mutations/transcripts.ts
548
+ function createTranscriptsMutationsAPI(client) {
549
+ return {
550
+ async delete(id) {
551
+ const mutation = `
552
+ mutation deleteTranscript($id: String!) {
553
+ deleteTranscript(id: $id) {
554
+ id
555
+ title
556
+ organizer_email
557
+ date
558
+ duration
559
+ }
560
+ }
561
+ `;
562
+ const data = await client.execute(mutation, { id });
563
+ return data.deleteTranscript;
564
+ }
565
+ };
566
+ }
567
+
568
+ // src/graphql/mutations/users.ts
569
+ function createUsersMutationsAPI(client) {
570
+ return {
571
+ async setRole(userId, role) {
572
+ const mutation = `
573
+ mutation setUserRole($userId: String!, $role: Role!) {
574
+ setUserRole(user_id: $userId, role: $role) {
575
+ id
576
+ name
577
+ email
578
+ role
579
+ }
580
+ }
581
+ `;
582
+ const data = await client.execute(mutation, {
583
+ userId,
584
+ role
585
+ });
586
+ return data.setUserRole;
587
+ }
588
+ };
589
+ }
590
+
591
+ // src/helpers/pagination.ts
592
+ async function* paginate(fetcher, pageSize = 50) {
593
+ let skip = 0;
594
+ let hasMore = true;
595
+ while (hasMore) {
596
+ const page = await fetcher(skip, pageSize);
597
+ for (const item of page) {
598
+ yield item;
599
+ }
600
+ if (page.length < pageSize) {
601
+ hasMore = false;
602
+ } else {
603
+ skip += pageSize;
604
+ }
605
+ }
606
+ }
607
+
608
+ // src/graphql/queries/ai-apps.ts
609
+ var AI_APP_OUTPUT_FIELDS = `
610
+ transcript_id
611
+ user_id
612
+ app_id
613
+ created_at
614
+ title
615
+ prompt
616
+ response
617
+ `;
618
+ function createAIAppsAPI(client) {
619
+ return {
620
+ async list(params) {
621
+ const query = `
622
+ query GetAIAppsOutputs(
623
+ $appId: String
624
+ $transcriptId: String
625
+ $skip: Float
626
+ $limit: Float
627
+ ) {
628
+ apps(
629
+ app_id: $appId
630
+ transcript_id: $transcriptId
631
+ skip: $skip
632
+ limit: $limit
633
+ ) {
634
+ outputs { ${AI_APP_OUTPUT_FIELDS} }
635
+ }
636
+ }
637
+ `;
638
+ const data = await client.execute(query, {
639
+ appId: params?.app_id,
640
+ transcriptId: params?.transcript_id,
641
+ skip: params?.skip,
642
+ limit: params?.limit ?? 10
643
+ });
644
+ return data.apps.outputs;
645
+ },
646
+ listAll(params) {
647
+ return paginate((skip, limit) => this.list({ ...params, skip, limit }), 10);
648
+ }
649
+ };
650
+ }
651
+
652
+ // src/graphql/queries/bites.ts
653
+ var BITE_FIELDS = `
654
+ id
655
+ transcript_id
656
+ user_id
657
+ name
658
+ status
659
+ summary
660
+ summary_status
661
+ media_type
662
+ start_time
663
+ end_time
664
+ created_at
665
+ thumbnail
666
+ preview
667
+ captions {
668
+ index
669
+ text
670
+ start_time
671
+ end_time
672
+ speaker_id
673
+ speaker_name
674
+ }
675
+ sources {
676
+ src
677
+ type
678
+ }
679
+ user {
680
+ id
681
+ name
682
+ first_name
683
+ last_name
684
+ picture
685
+ }
686
+ created_from {
687
+ id
688
+ name
689
+ type
690
+ description
691
+ duration
692
+ }
693
+ privacies
694
+ `;
695
+ function createBitesAPI(client) {
696
+ return {
697
+ async get(id) {
698
+ const query = `
699
+ query Bite($biteId: ID!) {
700
+ bite(id: $biteId) { ${BITE_FIELDS} }
701
+ }
702
+ `;
703
+ const data = await client.execute(query, { biteId: id });
704
+ return data.bite;
705
+ },
706
+ async list(params) {
707
+ const query = `
708
+ query Bites(
709
+ $transcriptId: ID
710
+ $mine: Boolean
711
+ $myTeam: Boolean
712
+ $limit: Int
713
+ $skip: Int
714
+ ) {
715
+ bites(
716
+ transcript_id: $transcriptId
717
+ mine: $mine
718
+ my_team: $myTeam
719
+ limit: $limit
720
+ skip: $skip
721
+ ) { ${BITE_FIELDS} }
722
+ }
723
+ `;
724
+ const data = await client.execute(query, {
725
+ transcriptId: params.transcript_id,
726
+ mine: params.mine,
727
+ myTeam: params.my_team,
728
+ limit: params.limit ?? 50,
729
+ skip: params.skip
730
+ });
731
+ return data.bites;
732
+ },
733
+ listAll(params) {
734
+ return paginate((skip, limit) => this.list({ ...params, skip, limit }), 50);
735
+ },
736
+ async create(params) {
737
+ const mutation = `
738
+ mutation CreateBite(
739
+ $transcriptId: ID!
740
+ $startTime: Float!
741
+ $endTime: Float!
742
+ $name: String
743
+ $mediaType: String
744
+ $summary: String
745
+ $privacies: [BitePrivacy!]
746
+ ) {
747
+ createBite(
748
+ transcript_Id: $transcriptId
749
+ start_time: $startTime
750
+ end_time: $endTime
751
+ name: $name
752
+ media_type: $mediaType
753
+ summary: $summary
754
+ privacies: $privacies
755
+ ) {
756
+ id
757
+ name
758
+ status
759
+ summary
760
+ }
761
+ }
762
+ `;
763
+ const data = await client.execute(mutation, {
764
+ transcriptId: params.transcript_id,
765
+ startTime: params.start_time,
766
+ endTime: params.end_time,
767
+ name: params.name,
768
+ mediaType: params.media_type,
769
+ summary: params.summary,
770
+ privacies: params.privacies
771
+ });
772
+ return data.createBite;
773
+ }
774
+ };
775
+ }
776
+
777
+ // src/graphql/queries/meetings.ts
778
+ var ACTIVE_MEETING_FIELDS = `
779
+ id
780
+ title
781
+ organizer_email
782
+ meeting_link
783
+ start_time
784
+ end_time
785
+ privacy
786
+ state
787
+ `;
788
+ function createMeetingsAPI(client) {
789
+ return {
790
+ async active(params) {
791
+ const query = `
792
+ query ActiveMeetings($email: String, $states: [MeetingState!]) {
793
+ active_meetings(input: { email: $email, states: $states }) {
794
+ ${ACTIVE_MEETING_FIELDS}
795
+ }
796
+ }
797
+ `;
798
+ const data = await client.execute(query, {
799
+ email: params?.email,
800
+ states: params?.states
801
+ });
802
+ return data.active_meetings;
803
+ },
804
+ async addBot(params) {
805
+ const mutation = `
806
+ mutation AddToLiveMeeting(
807
+ $meetingLink: String!
808
+ $title: String
809
+ $meetingPassword: String
810
+ $duration: Int
811
+ $language: String
812
+ ) {
813
+ addToLiveMeeting(
814
+ meeting_link: $meetingLink
815
+ title: $title
816
+ meeting_password: $meetingPassword
817
+ duration: $duration
818
+ language: $language
819
+ ) {
820
+ success
821
+ }
822
+ }
823
+ `;
824
+ const data = await client.execute(mutation, {
825
+ meetingLink: params.meeting_link,
826
+ title: params.title,
827
+ meetingPassword: params.password,
828
+ duration: params.duration,
829
+ language: params.language
830
+ });
831
+ return data.addToLiveMeeting;
832
+ }
833
+ };
834
+ }
835
+
836
+ // src/helpers/action-items.ts
837
+ var ASSIGNEE_PATTERNS = [
838
+ { pattern: /@(\w+)/i, group: 1 },
839
+ // @Alice
840
+ { pattern: /^(\w+):/i, group: 1 },
841
+ // Alice: at start
842
+ { pattern: /assigned to (\w+)/i, group: 1 },
843
+ // assigned to Alice
844
+ { pattern: /(\w+) will\b/i, group: 1 },
845
+ // Alice will
846
+ { pattern: /(\w+) to\b/i, group: 1 },
847
+ // Alice to (do something)
848
+ { pattern: /\s-\s*(\w+)$/i, group: 1 }
849
+ // ... - Alice
850
+ ];
851
+ var DUE_DATE_PATTERNS = [
852
+ { pattern: /by (monday|tuesday|wednesday|thursday|friday|saturday|sunday)/i, group: 1 },
853
+ { pattern: /by (tomorrow|today)/i, group: 1 },
854
+ { pattern: /by (EOD|end of day)/i, group: 1 },
855
+ { pattern: /by (EOW|end of week)/i, group: 1 },
856
+ { pattern: /due (\d{4}-\d{2}-\d{2})/i, group: 1 },
857
+ { pattern: /due (jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)\s+\d+/i, group: 0 },
858
+ { pattern: /by (\d{1,2}\/\d{1,2})/i, group: 1 }
859
+ ];
860
+ var LIST_PREFIX_PATTERN = /^(?:[-•*]|\d+\.)\s*/;
861
+ var SECTION_HEADER_PATTERN = /^\*\*(.+)\*\*$/;
862
+ function extractActionItems(transcript, options = {}) {
863
+ const actionItemsText = transcript.summary?.action_items;
864
+ if (!actionItemsText || actionItemsText.trim().length === 0) {
865
+ return emptyResult();
866
+ }
867
+ const config = {
868
+ detectAssignees: options.detectAssignees ?? true,
869
+ detectDueDates: options.detectDueDates ?? true,
870
+ includeSourceSentences: options.includeSourceSentences ?? false,
871
+ participantNames: options.participantNames ?? []
872
+ };
873
+ const taskSentences = config.includeSourceSentences ? buildTaskSentenceLookup(transcript) : [];
874
+ const lines = actionItemsText.split(/\n/);
875
+ return parseAllLines(lines, config, taskSentences);
876
+ }
877
+ function parseAllLines(lines, config, taskSentences) {
878
+ const items = [];
879
+ const assigneeSet = /* @__PURE__ */ new Set();
880
+ let currentSectionAssignee;
881
+ for (let i = 0; i < lines.length; i++) {
882
+ const result = processLine(lines[i], i + 1, config, taskSentences, currentSectionAssignee);
883
+ if (result.type === "header") {
884
+ currentSectionAssignee = result.assignee;
885
+ } else if (result.type === "item" && result.item) {
886
+ items.push(result.item);
887
+ if (result.item.assignee) {
888
+ assigneeSet.add(result.item.assignee);
889
+ }
890
+ }
891
+ }
892
+ return buildResult(items, assigneeSet);
893
+ }
894
+ function processLine(line, lineNumber, config, taskSentences, sectionAssignee) {
895
+ if (!line) return { type: "skip" };
896
+ const trimmed = line.trim();
897
+ if (trimmed.length === 0) return { type: "skip" };
898
+ const headerMatch = trimmed.match(SECTION_HEADER_PATTERN);
899
+ if (headerMatch?.[1]) {
900
+ const headerName = headerMatch[1];
901
+ const assignee = headerName.toLowerCase() === "unassigned" ? void 0 : headerName;
902
+ return { type: "header", assignee };
903
+ }
904
+ const item = parseLine(line, lineNumber, config, taskSentences, sectionAssignee);
905
+ if (item) {
906
+ return { type: "item", item };
907
+ }
908
+ return { type: "skip" };
909
+ }
910
+ function parseLine(line, lineNumber, config, taskSentences, sectionAssignee) {
911
+ if (!line) return null;
912
+ const trimmed = line.trim();
913
+ if (trimmed.length === 0) return null;
914
+ const text = trimmed.replace(LIST_PREFIX_PATTERN, "");
915
+ if (text.length === 0) return null;
916
+ const inlineAssignee = config.detectAssignees ? detectAssignee(text, config.participantNames) : void 0;
917
+ const assignee = inlineAssignee ?? sectionAssignee;
918
+ const dueDate = config.detectDueDates ? detectDueDate(text) : void 0;
919
+ const sourceSentence = config.includeSourceSentences ? findSourceSentence(text, taskSentences) : void 0;
920
+ return { text, assignee, dueDate, lineNumber, sourceSentence };
921
+ }
922
+ function buildResult(items, assigneeSet) {
923
+ return {
924
+ items,
925
+ totalItems: items.length,
926
+ assignedItems: items.filter((i) => i.assignee !== void 0).length,
927
+ datedItems: items.filter((i) => i.dueDate !== void 0).length,
928
+ assignees: Array.from(assigneeSet)
929
+ };
930
+ }
931
+ function emptyResult() {
932
+ return {
933
+ items: [],
934
+ totalItems: 0,
935
+ assignedItems: 0,
936
+ datedItems: 0,
937
+ assignees: []
938
+ };
939
+ }
940
+ function detectAssignee(text, participantNames) {
941
+ const participantSet = new Set(participantNames.map((n) => n.toLowerCase()));
942
+ const filterByParticipants = participantSet.size > 0;
943
+ for (const { pattern, group } of ASSIGNEE_PATTERNS) {
944
+ const match = text.match(pattern);
945
+ if (match?.[group]) {
946
+ const name = match[group];
947
+ if (filterByParticipants) {
948
+ if (participantSet.has(name.toLowerCase())) {
949
+ return name;
950
+ }
951
+ continue;
952
+ }
953
+ return name;
954
+ }
955
+ }
956
+ return void 0;
957
+ }
958
+ function detectDueDate(text) {
959
+ for (const { pattern, group } of DUE_DATE_PATTERNS) {
960
+ const match = text.match(pattern);
961
+ if (match) {
962
+ if (group === 0 && match[0]) {
963
+ const fullMatch = match[0];
964
+ const dateMatch = fullMatch.match(
965
+ /(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)\s+\d+/i
966
+ );
967
+ if (dateMatch?.[0]) {
968
+ return dateMatch[0];
969
+ }
970
+ }
971
+ if (match[group]) {
972
+ return match[group];
973
+ }
974
+ }
975
+ }
976
+ return void 0;
977
+ }
978
+ function buildTaskSentenceLookup(transcript) {
979
+ const sentences = transcript.sentences ?? [];
980
+ const result = [];
981
+ for (const sentence of sentences) {
982
+ const task = sentence.ai_filters?.task;
983
+ if (task) {
984
+ result.push({
985
+ text: sentence.text,
986
+ task: task.toLowerCase(),
987
+ speakerName: sentence.speaker_name,
988
+ startTime: Number.parseFloat(sentence.start_time)
989
+ });
990
+ }
991
+ }
992
+ return result;
993
+ }
994
+ function findSourceSentence(actionItemText, taskSentences) {
995
+ const normalizedItem = actionItemText.toLowerCase();
996
+ for (const sentence of taskSentences) {
997
+ if (normalizedItem.includes(sentence.task) || sentence.task.includes(normalizedItem)) {
998
+ return {
999
+ speakerName: sentence.speakerName,
1000
+ text: sentence.text,
1001
+ startTime: sentence.startTime
1002
+ };
1003
+ }
1004
+ const itemWords = new Set(normalizedItem.split(/\s+/).filter((w) => w.length > 3));
1005
+ const taskWords = sentence.task.split(/\s+/).filter((w) => w.length > 3);
1006
+ const matchingWords = taskWords.filter((w) => itemWords.has(w));
1007
+ if (taskWords.length > 0 && matchingWords.length / taskWords.length >= 0.5) {
1008
+ return {
1009
+ speakerName: sentence.speakerName,
1010
+ text: sentence.text,
1011
+ startTime: sentence.startTime
1012
+ };
1013
+ }
1014
+ }
1015
+ return void 0;
1016
+ }
1017
+
1018
+ // src/helpers/action-items-format.ts
1019
+ function filterActionItems(items, options) {
1020
+ const { assignees, assignedOnly, datedOnly } = options;
1021
+ const normalizedAssignees = assignees?.map((a) => a.toLowerCase());
1022
+ return items.filter((item) => {
1023
+ if (normalizedAssignees && normalizedAssignees.length > 0) {
1024
+ if (!item.assignee) return false;
1025
+ if (!normalizedAssignees.includes(item.assignee.toLowerCase())) return false;
1026
+ }
1027
+ if (assignedOnly && !item.assignee) {
1028
+ return false;
1029
+ }
1030
+ if (datedOnly && !item.dueDate) {
1031
+ return false;
1032
+ }
1033
+ return true;
1034
+ });
1035
+ }
1036
+ function aggregateActionItems(transcripts, extractionOptions, filterOptions) {
1037
+ if (transcripts.length === 0) {
1038
+ return emptyAggregatedResult();
1039
+ }
1040
+ const allItems = [];
1041
+ let transcriptsWithItems = 0;
1042
+ for (const transcript of transcripts) {
1043
+ const extracted = extractActionItems(transcript, extractionOptions);
1044
+ if (extracted.items.length > 0) {
1045
+ transcriptsWithItems++;
1046
+ for (const item of extracted.items) {
1047
+ allItems.push({
1048
+ ...item,
1049
+ transcriptId: transcript.id,
1050
+ transcriptTitle: transcript.title,
1051
+ transcriptDate: transcript.dateString
1052
+ });
1053
+ }
1054
+ }
1055
+ }
1056
+ const filteredItems = filterOptions ? filterActionItems(allItems, filterOptions) : allItems;
1057
+ return buildAggregatedResult(filteredItems, transcripts.length, transcriptsWithItems);
1058
+ }
1059
+ function emptyAggregatedResult() {
1060
+ return {
1061
+ items: [],
1062
+ totalItems: 0,
1063
+ transcriptsProcessed: 0,
1064
+ transcriptsWithItems: 0,
1065
+ assignedItems: 0,
1066
+ datedItems: 0,
1067
+ assignees: [],
1068
+ dateRange: { earliest: "", latest: "" }
1069
+ };
1070
+ }
1071
+ function buildAggregatedResult(items, transcriptsProcessed, transcriptsWithItems) {
1072
+ const assigneeSet = /* @__PURE__ */ new Set();
1073
+ let assignedItems = 0;
1074
+ let datedItems = 0;
1075
+ for (const item of items) {
1076
+ if (item.assignee) {
1077
+ assigneeSet.add(item.assignee);
1078
+ assignedItems++;
1079
+ }
1080
+ if (item.dueDate) {
1081
+ datedItems++;
1082
+ }
1083
+ }
1084
+ const dates = items.map((i) => i.transcriptDate).filter(Boolean).sort();
1085
+ return {
1086
+ items,
1087
+ totalItems: items.length,
1088
+ transcriptsProcessed,
1089
+ transcriptsWithItems,
1090
+ assignedItems,
1091
+ datedItems,
1092
+ assignees: Array.from(assigneeSet),
1093
+ dateRange: {
1094
+ earliest: dates[0] ?? "",
1095
+ latest: dates[dates.length - 1] ?? ""
1096
+ }
1097
+ };
1098
+ }
1099
+
1100
+ // src/helpers/domain-utils.ts
1101
+ function extractDomain(email) {
1102
+ const atIndex = email.indexOf("@");
1103
+ if (atIndex < 0) return "";
1104
+ const domain = email.slice(atIndex + 1).toLowerCase();
1105
+ return domain || "";
1106
+ }
1107
+ function hasExternalParticipants(participants, internalDomain) {
1108
+ const normalizedInternal = internalDomain.toLowerCase();
1109
+ return participants.some((email) => {
1110
+ const domain = extractDomain(email);
1111
+ return domain !== "" && domain !== normalizedInternal;
1112
+ });
1113
+ }
1114
+
1115
+ // src/helpers/meeting-insights.ts
1116
+ function analyzeMeetings(transcripts, options = {}) {
1117
+ const { speakers, groupBy, topSpeakersCount = 10, topParticipantsCount = 10 } = options;
1118
+ if (transcripts.length === 0) {
1119
+ return emptyInsights();
1120
+ }
1121
+ const totalDurationMinutes = sumDurations(transcripts);
1122
+ const averageDurationMinutes = totalDurationMinutes / transcripts.length;
1123
+ const byDayOfWeek = calculateDayOfWeekStats(transcripts);
1124
+ const byTimeGroup = groupBy ? calculateTimeGroupStats(transcripts, groupBy) : void 0;
1125
+ const participantData = aggregateParticipants(transcripts);
1126
+ const totalUniqueParticipants = participantData.uniqueEmails.size;
1127
+ const averageParticipantsPerMeeting = calculateAverageParticipants(transcripts);
1128
+ const topParticipants = buildTopParticipants(participantData.stats, topParticipantsCount);
1129
+ const speakerData = aggregateSpeakers(transcripts, speakers);
1130
+ const totalUniqueSpeakers = speakerData.uniqueNames.size;
1131
+ const topSpeakers = buildTopSpeakers(speakerData.stats, topSpeakersCount);
1132
+ const { earliestMeeting, latestMeeting } = findDateRange(transcripts);
1133
+ return {
1134
+ totalMeetings: transcripts.length,
1135
+ totalDurationMinutes,
1136
+ averageDurationMinutes,
1137
+ byDayOfWeek,
1138
+ byTimeGroup,
1139
+ totalUniqueParticipants,
1140
+ averageParticipantsPerMeeting,
1141
+ topParticipants,
1142
+ totalUniqueSpeakers,
1143
+ topSpeakers,
1144
+ earliestMeeting,
1145
+ latestMeeting
1146
+ };
1147
+ }
1148
+ function emptyInsights() {
1149
+ return {
1150
+ totalMeetings: 0,
1151
+ totalDurationMinutes: 0,
1152
+ averageDurationMinutes: 0,
1153
+ byDayOfWeek: emptyDayOfWeekStats(),
1154
+ byTimeGroup: void 0,
1155
+ totalUniqueParticipants: 0,
1156
+ averageParticipantsPerMeeting: 0,
1157
+ topParticipants: [],
1158
+ totalUniqueSpeakers: 0,
1159
+ topSpeakers: [],
1160
+ earliestMeeting: "",
1161
+ latestMeeting: ""
1162
+ };
1163
+ }
1164
+ function emptyDayOfWeekStats() {
1165
+ const emptyDay = () => ({ count: 0, totalMinutes: 0 });
1166
+ return {
1167
+ monday: emptyDay(),
1168
+ tuesday: emptyDay(),
1169
+ wednesday: emptyDay(),
1170
+ thursday: emptyDay(),
1171
+ friday: emptyDay(),
1172
+ saturday: emptyDay(),
1173
+ sunday: emptyDay()
1174
+ };
1175
+ }
1176
+ function sumDurations(transcripts) {
1177
+ return transcripts.reduce((sum, t) => sum + (t.duration ?? 0), 0);
1178
+ }
1179
+ function calculateDayOfWeekStats(transcripts) {
1180
+ const stats = emptyDayOfWeekStats();
1181
+ const dayNames = [
1182
+ "sunday",
1183
+ "monday",
1184
+ "tuesday",
1185
+ "wednesday",
1186
+ "thursday",
1187
+ "friday",
1188
+ "saturday"
1189
+ ];
1190
+ for (const t of transcripts) {
1191
+ const date = parseDate(t.dateString);
1192
+ if (!date) continue;
1193
+ const dayIndex = date.getUTCDay();
1194
+ const dayName = dayNames[dayIndex];
1195
+ if (dayName) {
1196
+ stats[dayName].count++;
1197
+ stats[dayName].totalMinutes += t.duration ?? 0;
1198
+ }
1199
+ }
1200
+ return stats;
1201
+ }
1202
+ function calculateTimeGroupStats(transcripts, groupBy) {
1203
+ const groups = /* @__PURE__ */ new Map();
1204
+ for (const t of transcripts) {
1205
+ const date = parseDate(t.dateString);
1206
+ if (!date) continue;
1207
+ const period = formatPeriod(date, groupBy);
1208
+ const existing = groups.get(period) ?? { count: 0, totalMinutes: 0 };
1209
+ existing.count++;
1210
+ existing.totalMinutes += t.duration ?? 0;
1211
+ groups.set(period, existing);
1212
+ }
1213
+ const result = [];
1214
+ for (const [period, data] of groups) {
1215
+ result.push({
1216
+ period,
1217
+ count: data.count,
1218
+ totalMinutes: data.totalMinutes,
1219
+ averageMinutes: data.totalMinutes / data.count
1220
+ });
1221
+ }
1222
+ result.sort((a, b) => a.period.localeCompare(b.period));
1223
+ return result;
1224
+ }
1225
+ function formatPeriod(date, groupBy) {
1226
+ const year = date.getUTCFullYear();
1227
+ const month = String(date.getUTCMonth() + 1).padStart(2, "0");
1228
+ const day = String(date.getUTCDate()).padStart(2, "0");
1229
+ switch (groupBy) {
1230
+ case "day":
1231
+ return `${year}-${month}-${day}`;
1232
+ case "week":
1233
+ return getISOWeek(date);
1234
+ case "month":
1235
+ return `${year}-${month}`;
1236
+ }
1237
+ }
1238
+ function getISOWeek(date) {
1239
+ const d = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
1240
+ d.setUTCDate(d.getUTCDate() + 4 - (d.getUTCDay() || 7));
1241
+ const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
1242
+ const weekNumber = Math.ceil(((d.getTime() - yearStart.getTime()) / 864e5 + 1) / 7);
1243
+ return `${d.getUTCFullYear()}-W${String(weekNumber).padStart(2, "0")}`;
1244
+ }
1245
+ function aggregateParticipants(transcripts) {
1246
+ const uniqueEmails = /* @__PURE__ */ new Set();
1247
+ const stats = /* @__PURE__ */ new Map();
1248
+ for (const t of transcripts) {
1249
+ const participants = t.participants ?? [];
1250
+ const seenInMeeting = /* @__PURE__ */ new Set();
1251
+ for (const email of participants) {
1252
+ const normalizedEmail = email.toLowerCase();
1253
+ uniqueEmails.add(normalizedEmail);
1254
+ if (seenInMeeting.has(normalizedEmail)) continue;
1255
+ seenInMeeting.add(normalizedEmail);
1256
+ const existing = stats.get(normalizedEmail) ?? { meetingCount: 0, totalMinutes: 0 };
1257
+ existing.meetingCount++;
1258
+ existing.totalMinutes += t.duration ?? 0;
1259
+ stats.set(normalizedEmail, existing);
1260
+ }
1261
+ }
1262
+ return { uniqueEmails, stats };
1263
+ }
1264
+ function calculateAverageParticipants(transcripts) {
1265
+ if (transcripts.length === 0) return 0;
1266
+ let totalParticipants = 0;
1267
+ for (const t of transcripts) {
1268
+ const unique = new Set((t.participants ?? []).map((p) => p.toLowerCase()));
1269
+ totalParticipants += unique.size;
1270
+ }
1271
+ return totalParticipants / transcripts.length;
1272
+ }
1273
+ function buildTopParticipants(stats, limit) {
1274
+ const result = [];
1275
+ for (const [email, data] of stats) {
1276
+ result.push({
1277
+ email,
1278
+ meetingCount: data.meetingCount,
1279
+ totalMinutes: data.totalMinutes
1280
+ });
1281
+ }
1282
+ result.sort((a, b) => b.meetingCount - a.meetingCount);
1283
+ return result.slice(0, limit);
1284
+ }
1285
+ function aggregateSpeakers(transcripts, filterSpeakers) {
1286
+ const uniqueNames = /* @__PURE__ */ new Set();
1287
+ const stats = /* @__PURE__ */ new Map();
1288
+ const filterSet = filterSpeakers ? new Set(filterSpeakers) : null;
1289
+ for (const t of transcripts) {
1290
+ const sentences = t.sentences ?? [];
1291
+ for (const sentence of sentences) {
1292
+ const speakerName = sentence.speaker_name;
1293
+ if (filterSet && !filterSet.has(speakerName)) continue;
1294
+ uniqueNames.add(speakerName);
1295
+ const existing = stats.get(speakerName) ?? {
1296
+ meetingCount: 0,
1297
+ totalTalkTimeSeconds: 0,
1298
+ meetings: /* @__PURE__ */ new Set()
1299
+ };
1300
+ const duration = parseSentenceDuration(sentence);
1301
+ existing.totalTalkTimeSeconds += duration;
1302
+ if (!existing.meetings.has(t.id)) {
1303
+ existing.meetings.add(t.id);
1304
+ existing.meetingCount++;
1305
+ }
1306
+ stats.set(speakerName, existing);
1307
+ }
1308
+ }
1309
+ return { uniqueNames, stats };
1310
+ }
1311
+ function parseSentenceDuration(sentence) {
1312
+ const start = Number.parseFloat(sentence.start_time);
1313
+ const end = Number.parseFloat(sentence.end_time);
1314
+ return Math.max(0, end - start);
1315
+ }
1316
+ function buildTopSpeakers(stats, limit) {
1317
+ const result = [];
1318
+ for (const [name, data] of stats) {
1319
+ result.push({
1320
+ name,
1321
+ meetingCount: data.meetingCount,
1322
+ totalTalkTimeSeconds: data.totalTalkTimeSeconds,
1323
+ averageTalkTimeSeconds: data.meetingCount > 0 ? data.totalTalkTimeSeconds / data.meetingCount : 0
1324
+ });
1325
+ }
1326
+ result.sort((a, b) => b.totalTalkTimeSeconds - a.totalTalkTimeSeconds);
1327
+ return result.slice(0, limit);
1328
+ }
1329
+ function findDateRange(transcripts) {
1330
+ let earliest = null;
1331
+ let latest = null;
1332
+ for (const t of transcripts) {
1333
+ const date = parseDate(t.dateString);
1334
+ if (!date) continue;
1335
+ if (!earliest || date < earliest) {
1336
+ earliest = date;
1337
+ }
1338
+ if (!latest || date > latest) {
1339
+ latest = date;
1340
+ }
1341
+ }
1342
+ return {
1343
+ earliestMeeting: earliest ? formatDateOnly(earliest) : "",
1344
+ latestMeeting: latest ? formatDateOnly(latest) : ""
1345
+ };
1346
+ }
1347
+ function parseDate(dateString) {
1348
+ if (!dateString) return null;
1349
+ const date = new Date(dateString);
1350
+ return Number.isNaN(date.getTime()) ? null : date;
1351
+ }
1352
+ function formatDateOnly(date) {
1353
+ const year = date.getUTCFullYear();
1354
+ const month = String(date.getUTCMonth() + 1).padStart(2, "0");
1355
+ const day = String(date.getUTCDate()).padStart(2, "0");
1356
+ return `${year}-${month}-${day}`;
1357
+ }
1358
+
1359
+ // src/helpers/search.ts
1360
+ function escapeRegex(str) {
1361
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1362
+ }
1363
+ function matchesSpeaker(sentence, speakerSet) {
1364
+ if (!speakerSet) return true;
1365
+ return speakerSet.has(sentence.speaker_name.toLowerCase());
1366
+ }
1367
+ function matchesAIFilters(sentence, filterQuestions, filterTasks) {
1368
+ if (!filterQuestions && !filterTasks) return true;
1369
+ const hasQuestion = Boolean(sentence.ai_filters?.question);
1370
+ const hasTask = Boolean(sentence.ai_filters?.task);
1371
+ if (filterQuestions && filterTasks) {
1372
+ return hasQuestion || hasTask;
1373
+ }
1374
+ if (filterQuestions) return hasQuestion;
1375
+ if (filterTasks) return hasTask;
1376
+ return true;
1377
+ }
1378
+ function extractContext(sentences, index, contextLines) {
1379
+ const beforeStart = Math.max(0, index - contextLines);
1380
+ const afterEnd = Math.min(sentences.length, index + contextLines + 1);
1381
+ return {
1382
+ before: sentences.slice(beforeStart, index).map((s) => ({
1383
+ speakerName: s.speaker_name,
1384
+ text: s.text
1385
+ })),
1386
+ after: sentences.slice(index + 1, afterEnd).map((s) => ({
1387
+ speakerName: s.speaker_name,
1388
+ text: s.text
1389
+ }))
1390
+ };
1391
+ }
1392
+ function sentenceToMatch(sentence, transcript, context) {
1393
+ return {
1394
+ transcriptId: transcript.id,
1395
+ transcriptTitle: transcript.title,
1396
+ transcriptDate: transcript.dateString,
1397
+ transcriptUrl: transcript.transcript_url,
1398
+ sentence: {
1399
+ index: sentence.index,
1400
+ text: sentence.text,
1401
+ speakerName: sentence.speaker_name,
1402
+ startTime: Number.parseFloat(sentence.start_time),
1403
+ endTime: Number.parseFloat(sentence.end_time),
1404
+ isQuestion: Boolean(sentence.ai_filters?.question),
1405
+ isTask: Boolean(sentence.ai_filters?.task)
1406
+ },
1407
+ context
1408
+ };
1409
+ }
1410
+ function searchTranscript(transcript, options) {
1411
+ const {
1412
+ query,
1413
+ caseSensitive = false,
1414
+ speakers,
1415
+ filterQuestions = false,
1416
+ filterTasks = false,
1417
+ contextLines = 1
1418
+ } = options;
1419
+ if (!query || query.trim() === "") {
1420
+ return [];
1421
+ }
1422
+ const sentences = transcript.sentences ?? [];
1423
+ if (sentences.length === 0) {
1424
+ return [];
1425
+ }
1426
+ const escapedQuery = escapeRegex(query);
1427
+ const regex = new RegExp(escapedQuery, caseSensitive ? "" : "i");
1428
+ const speakerSet = speakers ? new Set(speakers.map((s) => s.toLowerCase())) : null;
1429
+ const matches = [];
1430
+ for (let i = 0; i < sentences.length; i++) {
1431
+ const sentence = sentences[i];
1432
+ if (!sentence) continue;
1433
+ if (!regex.test(sentence.text)) continue;
1434
+ if (!matchesSpeaker(sentence, speakerSet)) continue;
1435
+ if (!matchesAIFilters(sentence, filterQuestions, filterTasks)) continue;
1436
+ const context = extractContext(sentences, i, contextLines);
1437
+ matches.push(sentenceToMatch(sentence, transcript, context));
1438
+ }
1439
+ return matches;
1440
+ }
1441
+
1442
+ // src/graphql/queries/transcripts.ts
1443
+ var TRANSCRIPT_BASE_FIELDS = `
1444
+ id
1445
+ title
1446
+ organizer_email
1447
+ host_email
1448
+ user {
1449
+ user_id
1450
+ email
1451
+ name
1452
+ }
1453
+ speakers {
1454
+ id
1455
+ name
1456
+ }
1457
+ transcript_url
1458
+ participants
1459
+ meeting_attendees {
1460
+ displayName
1461
+ email
1462
+ phoneNumber
1463
+ name
1464
+ location
1465
+ }
1466
+ meeting_attendance {
1467
+ name
1468
+ join_time
1469
+ leave_time
1470
+ }
1471
+ fireflies_users
1472
+ workspace_users
1473
+ duration
1474
+ dateString
1475
+ date
1476
+ audio_url
1477
+ video_url
1478
+ calendar_id
1479
+ meeting_info {
1480
+ fred_joined
1481
+ silent_meeting
1482
+ summary_status
1483
+ }
1484
+ cal_id
1485
+ calendar_type
1486
+ apps_preview {
1487
+ outputs {
1488
+ transcript_id
1489
+ user_id
1490
+ app_id
1491
+ created_at
1492
+ title
1493
+ prompt
1494
+ response
1495
+ }
1496
+ }
1497
+ meeting_link
1498
+ analytics {
1499
+ sentiments {
1500
+ negative_pct
1501
+ neutral_pct
1502
+ positive_pct
1503
+ }
1504
+ }
1505
+ channels {
1506
+ id
1507
+ title
1508
+ is_private
1509
+ created_at
1510
+ updated_at
1511
+ created_by
1512
+ members {
1513
+ user_id
1514
+ email
1515
+ name
1516
+ }
1517
+ }
1518
+ `;
1519
+ var SENTENCES_FIELDS = `
1520
+ sentences {
1521
+ index
1522
+ text
1523
+ raw_text
1524
+ start_time
1525
+ end_time
1526
+ speaker_id
1527
+ speaker_name
1528
+ ai_filters {
1529
+ task
1530
+ pricing
1531
+ metric
1532
+ question
1533
+ date_and_time
1534
+ text_cleanup
1535
+ sentiment
1536
+ }
1537
+ }
1538
+ `;
1539
+ var SUMMARY_FIELDS = `
1540
+ summary {
1541
+ action_items
1542
+ keywords
1543
+ outline
1544
+ overview
1545
+ shorthand_bullet
1546
+ notes
1547
+ gist
1548
+ bullet_gist
1549
+ short_summary
1550
+ short_overview
1551
+ meeting_type
1552
+ topics_discussed
1553
+ transcript_chapters
1554
+ extended_sections {
1555
+ title
1556
+ content
1557
+ }
1558
+ }
1559
+ `;
1560
+ function buildTranscriptFields(params) {
1561
+ const includeSentences = params?.includeSentences !== false;
1562
+ const includeSummary = params?.includeSummary !== false;
1563
+ let fields = TRANSCRIPT_BASE_FIELDS;
1564
+ if (includeSentences) {
1565
+ fields += SENTENCES_FIELDS;
1566
+ }
1567
+ if (includeSummary) {
1568
+ fields += SUMMARY_FIELDS;
1569
+ }
1570
+ return fields;
1571
+ }
1572
+ var TRANSCRIPT_LIST_FIELDS = `
1573
+ id
1574
+ title
1575
+ organizer_email
1576
+ transcript_url
1577
+ participants
1578
+ duration
1579
+ dateString
1580
+ date
1581
+ video_url
1582
+ meeting_info {
1583
+ fred_joined
1584
+ silent_meeting
1585
+ summary_status
1586
+ }
1587
+ `;
1588
+ function createTranscriptsAPI(client) {
1589
+ return {
1590
+ async get(id, params) {
1591
+ const fields = buildTranscriptFields(params);
1592
+ const query = `
1593
+ query GetTranscript($id: String!) {
1594
+ transcript(id: $id) {
1595
+ ${fields}
1596
+ }
1597
+ }
1598
+ `;
1599
+ const data = await client.execute(query, { id });
1600
+ return normalizeTranscript(data.transcript);
1601
+ },
1602
+ async list(params) {
1603
+ const query = `
1604
+ query ListTranscripts(
1605
+ $keyword: String
1606
+ $scope: String
1607
+ $organizers: [String!]
1608
+ $participants: [String!]
1609
+ $user_id: String
1610
+ $mine: Boolean
1611
+ $channel_id: String
1612
+ $fromDate: DateTime
1613
+ $toDate: DateTime
1614
+ $limit: Int
1615
+ $skip: Int
1616
+ $title: String
1617
+ $host_email: String
1618
+ $organizer_email: String
1619
+ $participant_email: String
1620
+ $date: Float
1621
+ ) {
1622
+ transcripts(
1623
+ keyword: $keyword
1624
+ scope: $scope
1625
+ organizers: $organizers
1626
+ participants: $participants
1627
+ user_id: $user_id
1628
+ mine: $mine
1629
+ channel_id: $channel_id
1630
+ fromDate: $fromDate
1631
+ toDate: $toDate
1632
+ limit: $limit
1633
+ skip: $skip
1634
+ title: $title
1635
+ host_email: $host_email
1636
+ organizer_email: $organizer_email
1637
+ participant_email: $participant_email
1638
+ date: $date
1639
+ ) {
1640
+ ${TRANSCRIPT_LIST_FIELDS}
1641
+ }
1642
+ }
1643
+ `;
1644
+ const variables = buildListVariables(params);
1645
+ const data = await client.execute(query, variables);
1646
+ return data.transcripts.map(normalizeTranscript);
1647
+ },
1648
+ async getSummary(id) {
1649
+ const query = `
1650
+ query GetTranscriptSummary($id: String!) {
1651
+ transcript(id: $id) {
1652
+ summary {
1653
+ action_items
1654
+ keywords
1655
+ outline
1656
+ overview
1657
+ shorthand_bullet
1658
+ notes
1659
+ gist
1660
+ bullet_gist
1661
+ short_summary
1662
+ short_overview
1663
+ meeting_type
1664
+ topics_discussed
1665
+ transcript_chapters
1666
+ extended_sections {
1667
+ title
1668
+ content
1669
+ }
1670
+ }
1671
+ }
1672
+ }
1673
+ `;
1674
+ const data = await client.execute(query, { id });
1675
+ return data.transcript.summary;
1676
+ },
1677
+ listAll(params) {
1678
+ return paginate((skip, limit) => this.list({ ...params, skip, limit }), 50);
1679
+ },
1680
+ async search(query, params = {}) {
1681
+ const {
1682
+ caseSensitive = false,
1683
+ scope = "sentences",
1684
+ speakers,
1685
+ filterQuestions,
1686
+ filterTasks,
1687
+ contextLines = 1,
1688
+ limit,
1689
+ ...listParams
1690
+ } = params;
1691
+ const transcripts = [];
1692
+ for await (const t of this.listAll({
1693
+ keyword: query,
1694
+ scope,
1695
+ ...listParams
1696
+ })) {
1697
+ transcripts.push(t);
1698
+ if (limit && transcripts.length >= limit) break;
1699
+ }
1700
+ const allMatches = [];
1701
+ let transcriptsWithMatches = 0;
1702
+ for (const t of transcripts) {
1703
+ const full = await this.get(t.id, { includeSentences: true });
1704
+ const matches = searchTranscript(full, {
1705
+ query,
1706
+ caseSensitive,
1707
+ speakers,
1708
+ filterQuestions,
1709
+ filterTasks,
1710
+ contextLines
1711
+ });
1712
+ if (matches.length > 0) {
1713
+ transcriptsWithMatches++;
1714
+ allMatches.push(...matches);
1715
+ }
1716
+ }
1717
+ return {
1718
+ query,
1719
+ options: params,
1720
+ totalMatches: allMatches.length,
1721
+ transcriptsSearched: transcripts.length,
1722
+ transcriptsWithMatches,
1723
+ matches: allMatches
1724
+ };
1725
+ },
1726
+ async insights(params = {}) {
1727
+ const {
1728
+ fromDate,
1729
+ toDate,
1730
+ mine,
1731
+ organizers,
1732
+ participants,
1733
+ user_id,
1734
+ channel_id,
1735
+ limit,
1736
+ external,
1737
+ speakers,
1738
+ groupBy,
1739
+ topSpeakersCount,
1740
+ topParticipantsCount
1741
+ } = params;
1742
+ let internalDomain;
1743
+ if (external) {
1744
+ const userQuery = "query { user { email } }";
1745
+ const userData = await client.execute(userQuery);
1746
+ internalDomain = extractDomain(userData.user.email);
1747
+ }
1748
+ const transcripts = [];
1749
+ for await (const t of this.listAll({
1750
+ fromDate,
1751
+ toDate,
1752
+ mine,
1753
+ organizers,
1754
+ participants,
1755
+ user_id,
1756
+ channel_id
1757
+ })) {
1758
+ if (internalDomain && !hasExternalParticipants(t.participants, internalDomain)) {
1759
+ continue;
1760
+ }
1761
+ const full = await this.get(t.id, { includeSentences: true, includeSummary: false });
1762
+ transcripts.push(full);
1763
+ if (limit && transcripts.length >= limit) break;
1764
+ }
1765
+ return analyzeMeetings(transcripts, {
1766
+ speakers,
1767
+ groupBy,
1768
+ topSpeakersCount,
1769
+ topParticipantsCount
1770
+ });
1771
+ },
1772
+ async exportActionItems(params = {}) {
1773
+ const { fromDate, toDate, mine, organizers, participants, limit, filterOptions } = params;
1774
+ const transcripts = [];
1775
+ for await (const t of this.listAll({
1776
+ fromDate,
1777
+ toDate,
1778
+ mine,
1779
+ organizers,
1780
+ participants
1781
+ })) {
1782
+ const full = await this.get(t.id, { includeSentences: false, includeSummary: true });
1783
+ transcripts.push(full);
1784
+ if (limit && transcripts.length >= limit) break;
1785
+ }
1786
+ return aggregateActionItems(transcripts, {}, filterOptions);
1787
+ }
1788
+ };
1789
+ }
1790
+ function orUndefined(value) {
1791
+ return value ?? void 0;
1792
+ }
1793
+ function orEmptyArray(value) {
1794
+ return value ?? [];
1795
+ }
1796
+ function normalizeRequiredFields(raw) {
1797
+ return {
1798
+ id: raw.id,
1799
+ title: raw.title ?? "",
1800
+ organizer_email: raw.organizer_email ?? "",
1801
+ transcript_url: raw.transcript_url ?? "",
1802
+ duration: raw.duration ?? 0,
1803
+ dateString: raw.dateString ?? "",
1804
+ date: raw.date ?? 0
1805
+ };
1806
+ }
1807
+ function normalizeArrayFields(raw) {
1808
+ return {
1809
+ speakers: orEmptyArray(raw.speakers),
1810
+ participants: orEmptyArray(raw.participants),
1811
+ meeting_attendees: orEmptyArray(raw.meeting_attendees),
1812
+ meeting_attendance: orEmptyArray(raw.meeting_attendance),
1813
+ fireflies_users: orEmptyArray(raw.fireflies_users),
1814
+ workspace_users: orEmptyArray(raw.workspace_users),
1815
+ sentences: orEmptyArray(raw.sentences),
1816
+ channels: orEmptyArray(raw.channels)
1817
+ };
1818
+ }
1819
+ function normalizeOptionalFields(raw) {
1820
+ return {
1821
+ host_email: orUndefined(raw.host_email),
1822
+ user: orUndefined(raw.user),
1823
+ audio_url: orUndefined(raw.audio_url),
1824
+ video_url: orUndefined(raw.video_url),
1825
+ calendar_id: orUndefined(raw.calendar_id),
1826
+ summary: orUndefined(raw.summary),
1827
+ meeting_info: orUndefined(raw.meeting_info),
1828
+ cal_id: orUndefined(raw.cal_id),
1829
+ calendar_type: orUndefined(raw.calendar_type),
1830
+ apps_preview: orUndefined(raw.apps_preview),
1831
+ meeting_link: orUndefined(raw.meeting_link),
1832
+ analytics: orUndefined(raw.analytics)
1833
+ };
1834
+ }
1835
+ function normalizeTranscript(raw) {
1836
+ return {
1837
+ ...normalizeRequiredFields(raw),
1838
+ ...normalizeArrayFields(raw),
1839
+ ...normalizeOptionalFields(raw)
1840
+ };
1841
+ }
1842
+ function buildListVariables(params) {
1843
+ if (!params) {
1844
+ return { limit: 50 };
1845
+ }
1846
+ return {
1847
+ keyword: params.keyword,
1848
+ scope: params.scope,
1849
+ organizers: params.organizers,
1850
+ participants: params.participants,
1851
+ user_id: params.user_id,
1852
+ mine: params.mine,
1853
+ channel_id: params.channel_id,
1854
+ fromDate: params.fromDate,
1855
+ toDate: params.toDate,
1856
+ limit: params.limit ?? 50,
1857
+ skip: params.skip,
1858
+ title: params.title,
1859
+ host_email: params.host_email,
1860
+ organizer_email: params.organizer_email,
1861
+ participant_email: params.participant_email,
1862
+ date: params.date
1863
+ };
1864
+ }
1865
+
1866
+ // src/graphql/queries/users.ts
1867
+ var USER_FIELDS = `
1868
+ user_id
1869
+ email
1870
+ name
1871
+ num_transcripts
1872
+ recent_meeting
1873
+ recent_transcript
1874
+ minutes_consumed
1875
+ is_admin
1876
+ integrations
1877
+ user_groups {
1878
+ id
1879
+ name
1880
+ handle
1881
+ members {
1882
+ user_id
1883
+ email
1884
+ }
1885
+ }
1886
+ `;
1887
+ function createUsersAPI(client) {
1888
+ return {
1889
+ async me() {
1890
+ const query = `query { user { ${USER_FIELDS} } }`;
1891
+ const data = await client.execute(query);
1892
+ return data.user;
1893
+ },
1894
+ async get(id) {
1895
+ const query = `
1896
+ query User($userId: String!) {
1897
+ user(id: $userId) { ${USER_FIELDS} }
1898
+ }
1899
+ `;
1900
+ const data = await client.execute(query, { userId: id });
1901
+ return data.user;
1902
+ },
1903
+ async list() {
1904
+ const query = `query Users { users { ${USER_FIELDS} } }`;
1905
+ const data = await client.execute(query);
1906
+ return data.users;
1907
+ }
1908
+ };
1909
+ }
1910
+ var DEFAULT_WS_URL = "wss://api.fireflies.ai";
1911
+ var DEFAULT_WS_PATH = "/ws/realtime";
1912
+ var DEFAULT_TIMEOUT2 = 2e4;
1913
+ var DEFAULT_CHUNK_TIMEOUT = 2e4;
1914
+ var DEFAULT_RECONNECT_DELAY = 5e3;
1915
+ var DEFAULT_MAX_RECONNECT_DELAY = 6e4;
1916
+ var DEFAULT_MAX_RECONNECT_ATTEMPTS = 10;
1917
+ var RealtimeConnection = class {
1918
+ socket = null;
1919
+ config;
1920
+ constructor(config) {
1921
+ this.config = {
1922
+ wsUrl: DEFAULT_WS_URL,
1923
+ wsPath: DEFAULT_WS_PATH,
1924
+ timeout: DEFAULT_TIMEOUT2,
1925
+ chunkTimeout: DEFAULT_CHUNK_TIMEOUT,
1926
+ reconnect: true,
1927
+ maxReconnectAttempts: DEFAULT_MAX_RECONNECT_ATTEMPTS,
1928
+ reconnectDelay: DEFAULT_RECONNECT_DELAY,
1929
+ maxReconnectDelay: DEFAULT_MAX_RECONNECT_DELAY,
1930
+ ...config
1931
+ };
1932
+ }
1933
+ /**
1934
+ * Establish connection and wait for auth success.
1935
+ */
1936
+ async connect() {
1937
+ if (this.socket?.connected) {
1938
+ return;
1939
+ }
1940
+ const socket = socket_ioClient.io(this.config.wsUrl, {
1941
+ path: this.config.wsPath,
1942
+ auth: {
1943
+ token: `Bearer ${this.config.apiKey}`,
1944
+ transcriptId: this.config.transcriptId
1945
+ },
1946
+ // Force WebSocket transport (proven more reliable than polling)
1947
+ transports: ["websocket"],
1948
+ reconnection: this.config.reconnect,
1949
+ reconnectionDelay: this.config.reconnectDelay,
1950
+ reconnectionDelayMax: this.config.maxReconnectDelay,
1951
+ reconnectionAttempts: this.config.maxReconnectAttempts,
1952
+ // Exponential backoff factor (default 2x matches our fireflies-whiteboard pattern)
1953
+ randomizationFactor: 0.5,
1954
+ timeout: this.config.timeout,
1955
+ autoConnect: false
1956
+ });
1957
+ this.socket = socket;
1958
+ return new Promise((resolve, reject) => {
1959
+ const timeoutId = setTimeout(() => {
1960
+ socket.disconnect();
1961
+ reject(new TimeoutError(`Realtime connection timed out after ${this.config.timeout}ms`));
1962
+ }, this.config.timeout);
1963
+ const cleanup = () => clearTimeout(timeoutId);
1964
+ socket.once("auth.success", () => {
1965
+ cleanup();
1966
+ resolve();
1967
+ });
1968
+ socket.once("auth.failed", (data) => {
1969
+ cleanup();
1970
+ socket.disconnect();
1971
+ reject(new AuthenticationError(`Realtime auth failed: ${formatData(data)}`));
1972
+ });
1973
+ socket.once("connection.error", (data) => {
1974
+ cleanup();
1975
+ socket.disconnect();
1976
+ reject(new ConnectionError(`Realtime connection error: ${formatData(data)}`));
1977
+ });
1978
+ socket.once("connect_error", (error) => {
1979
+ cleanup();
1980
+ socket.disconnect();
1981
+ const message = error.message || "Connection failed";
1982
+ if (message.includes("auth") || message.includes("401") || message.includes("unauthorized")) {
1983
+ reject(new AuthenticationError(`Realtime auth failed: ${message}`));
1984
+ } else {
1985
+ reject(
1986
+ new ConnectionError(`Realtime connection failed: ${message}`, {
1987
+ cause: error
1988
+ })
1989
+ );
1990
+ }
1991
+ });
1992
+ socket.connect();
1993
+ });
1994
+ }
1995
+ /**
1996
+ * Register a chunk handler.
1997
+ * Handles both { payload: {...} } and direct payload shapes.
1998
+ */
1999
+ onChunk(handler) {
2000
+ this.socket?.on("transcription.broadcast", (data) => {
2001
+ const chunk = "payload" in data ? data.payload : data;
2002
+ handler(chunk);
2003
+ });
2004
+ }
2005
+ /**
2006
+ * Register a disconnect handler.
2007
+ */
2008
+ onDisconnect(handler) {
2009
+ this.socket?.on("disconnect", handler);
2010
+ }
2011
+ /**
2012
+ * Register a reconnect handler.
2013
+ */
2014
+ onReconnect(handler) {
2015
+ this.socket?.io.on("reconnect", handler);
2016
+ }
2017
+ /**
2018
+ * Register a reconnect attempt handler.
2019
+ */
2020
+ onReconnectAttempt(handler) {
2021
+ this.socket?.io.on("reconnect_attempt", handler);
2022
+ }
2023
+ /**
2024
+ * Register an error handler.
2025
+ */
2026
+ onError(handler) {
2027
+ this.socket?.on("connect_error", handler);
2028
+ }
2029
+ /**
2030
+ * Disconnect and cleanup.
2031
+ */
2032
+ disconnect() {
2033
+ if (this.socket) {
2034
+ this.socket.disconnect();
2035
+ this.socket = null;
2036
+ }
2037
+ }
2038
+ get connected() {
2039
+ return this.socket?.connected ?? false;
2040
+ }
2041
+ };
2042
+ function formatData(data) {
2043
+ if (data === void 0 || data === null) {
2044
+ return String(data);
2045
+ }
2046
+ try {
2047
+ return JSON.stringify(data);
2048
+ } catch {
2049
+ return String(data);
2050
+ }
2051
+ }
2052
+
2053
+ // src/realtime/stream.ts
2054
+ var RealtimeStream = class {
2055
+ connection;
2056
+ listeners = /* @__PURE__ */ new Map();
2057
+ buffer = [];
2058
+ waiters = [];
2059
+ closed = false;
2060
+ lastChunkId = null;
2061
+ lastChunk = null;
2062
+ constructor(config) {
2063
+ this.connection = new RealtimeConnection(config);
2064
+ }
2065
+ /**
2066
+ * Connect to the realtime stream.
2067
+ * @throws AuthenticationError if authentication fails
2068
+ * @throws ConnectionError if connection fails
2069
+ * @throws TimeoutError if connection times out
2070
+ */
2071
+ async connect() {
2072
+ await this.connection.connect();
2073
+ this.setupHandlers();
2074
+ this.emit("connected");
2075
+ }
2076
+ setupHandlers() {
2077
+ this.connection.onChunk((rawChunk) => {
2078
+ const isNewChunk = this.lastChunkId !== null && rawChunk.chunk_id !== this.lastChunkId;
2079
+ if (isNewChunk && this.lastChunk) {
2080
+ const finalChunk = { ...this.lastChunk, isFinal: true };
2081
+ this.emitChunk(finalChunk);
2082
+ }
2083
+ const chunk = { ...rawChunk, isFinal: false };
2084
+ this.lastChunkId = chunk.chunk_id;
2085
+ this.lastChunk = chunk;
2086
+ this.emit("chunk", chunk);
2087
+ });
2088
+ this.connection.onDisconnect((reason) => {
2089
+ if (this.lastChunk) {
2090
+ const finalChunk = { ...this.lastChunk, isFinal: true };
2091
+ this.emitChunk(finalChunk);
2092
+ this.lastChunk = null;
2093
+ }
2094
+ this.emit("disconnected", reason);
2095
+ if (!this.connection.connected) {
2096
+ this.closed = true;
2097
+ for (const waiter of this.waiters) {
2098
+ waiter(null);
2099
+ }
2100
+ this.waiters = [];
2101
+ }
2102
+ });
2103
+ this.connection.onReconnectAttempt((attempt) => {
2104
+ this.emit("reconnecting", attempt);
2105
+ });
2106
+ this.connection.onReconnect(() => {
2107
+ this.emit("connected");
2108
+ });
2109
+ this.connection.onError((error) => {
2110
+ this.emit("error", error);
2111
+ });
2112
+ }
2113
+ /**
2114
+ * Register an event listener.
2115
+ * @param event - Event name
2116
+ * @param handler - Event handler
2117
+ */
2118
+ on(event, handler) {
2119
+ let handlers = this.listeners.get(event);
2120
+ if (!handlers) {
2121
+ handlers = /* @__PURE__ */ new Set();
2122
+ this.listeners.set(event, handlers);
2123
+ }
2124
+ handlers.add(handler);
2125
+ return this;
2126
+ }
2127
+ /**
2128
+ * Remove an event listener.
2129
+ * @param event - Event name
2130
+ * @param handler - Event handler to remove
2131
+ */
2132
+ off(event, handler) {
2133
+ this.listeners.get(event)?.delete(handler);
2134
+ return this;
2135
+ }
2136
+ /**
2137
+ * Emit a chunk to both event listeners and async iterator buffer.
2138
+ * Used for final chunks that should be yielded by the iterator.
2139
+ */
2140
+ emitChunk(chunk) {
2141
+ this.emit("chunk", chunk);
2142
+ if (chunk.isFinal) {
2143
+ if (this.waiters.length > 0) {
2144
+ const waiter = this.waiters.shift();
2145
+ waiter?.(chunk);
2146
+ } else {
2147
+ this.buffer.push(chunk);
2148
+ }
2149
+ }
2150
+ }
2151
+ emit(event, ...args) {
2152
+ const handlers = this.listeners.get(event);
2153
+ handlers?.forEach((handler) => {
2154
+ try {
2155
+ handler(...args);
2156
+ } catch {
2157
+ }
2158
+ });
2159
+ }
2160
+ /**
2161
+ * AsyncIterable implementation for `for await` loops.
2162
+ */
2163
+ async *[Symbol.asyncIterator]() {
2164
+ if (this.closed) {
2165
+ throw new StreamClosedError();
2166
+ }
2167
+ while (!this.closed) {
2168
+ const buffered = this.buffer.shift();
2169
+ if (buffered) {
2170
+ yield buffered;
2171
+ continue;
2172
+ }
2173
+ const chunk = await new Promise((resolve) => {
2174
+ if (this.closed) {
2175
+ resolve(null);
2176
+ return;
2177
+ }
2178
+ this.waiters.push(resolve);
2179
+ });
2180
+ if (chunk === null) {
2181
+ break;
2182
+ }
2183
+ yield chunk;
2184
+ }
2185
+ }
2186
+ /**
2187
+ * Close the stream and disconnect.
2188
+ */
2189
+ close() {
2190
+ if (this.lastChunk) {
2191
+ const finalChunk = { ...this.lastChunk, isFinal: true };
2192
+ this.emitChunk(finalChunk);
2193
+ this.lastChunk = null;
2194
+ }
2195
+ this.closed = true;
2196
+ this.connection.disconnect();
2197
+ this.buffer = [];
2198
+ this.lastChunkId = null;
2199
+ for (const waiter of this.waiters) {
2200
+ waiter(null);
2201
+ }
2202
+ this.waiters = [];
2203
+ }
2204
+ /**
2205
+ * Whether the stream is currently connected.
2206
+ */
2207
+ get connected() {
2208
+ return this.connection.connected;
2209
+ }
2210
+ };
2211
+
2212
+ // src/realtime/api.ts
2213
+ function createRealtimeAPI(apiKey, baseConfig) {
2214
+ return {
2215
+ async connect(transcriptId) {
2216
+ const stream = new RealtimeStream({
2217
+ apiKey,
2218
+ transcriptId,
2219
+ ...baseConfig
2220
+ });
2221
+ await stream.connect();
2222
+ return stream;
2223
+ },
2224
+ async *stream(transcriptId) {
2225
+ const stream = new RealtimeStream({
2226
+ apiKey,
2227
+ transcriptId,
2228
+ ...baseConfig
2229
+ });
2230
+ try {
2231
+ await stream.connect();
2232
+ yield* stream;
2233
+ } finally {
2234
+ stream.close();
2235
+ }
2236
+ }
2237
+ };
2238
+ }
2239
+
2240
+ // src/client.ts
2241
+ var FirefliesClient = class {
2242
+ graphql;
2243
+ /**
2244
+ * Transcript operations: list, get, search, delete.
2245
+ */
2246
+ transcripts;
2247
+ /**
2248
+ * User operations: me, get, list, setRole.
2249
+ */
2250
+ users;
2251
+ /**
2252
+ * Bite operations: get, list, create.
2253
+ */
2254
+ bites;
2255
+ /**
2256
+ * Meeting operations: active meetings, add bot.
2257
+ */
2258
+ meetings;
2259
+ /**
2260
+ * Audio operations: upload audio for transcription.
2261
+ */
2262
+ audio;
2263
+ /**
2264
+ * AI Apps operations: list outputs.
2265
+ */
2266
+ aiApps;
2267
+ /**
2268
+ * Realtime transcription streaming.
2269
+ */
2270
+ realtime;
2271
+ /**
2272
+ * Create a new Fireflies client.
2273
+ *
2274
+ * @param config - Client configuration
2275
+ * @throws FirefliesError if API key is missing
2276
+ */
2277
+ constructor(config) {
2278
+ this.graphql = new GraphQLClient(config);
2279
+ const transcriptsQueries = createTranscriptsAPI(this.graphql);
2280
+ const transcriptsMutations = createTranscriptsMutationsAPI(this.graphql);
2281
+ this.transcripts = { ...transcriptsQueries, ...transcriptsMutations };
2282
+ const usersQueries = createUsersAPI(this.graphql);
2283
+ const usersMutations = createUsersMutationsAPI(this.graphql);
2284
+ this.users = { ...usersQueries, ...usersMutations };
2285
+ this.bites = createBitesAPI(this.graphql);
2286
+ this.meetings = createMeetingsAPI(this.graphql);
2287
+ this.audio = createAudioAPI(this.graphql);
2288
+ this.aiApps = createAIAppsAPI(this.graphql);
2289
+ this.realtime = createRealtimeAPI(config.apiKey);
2290
+ }
2291
+ /**
2292
+ * Get the current rate limit state.
2293
+ * Returns undefined if rate limit tracking is not configured.
2294
+ *
2295
+ * @example
2296
+ * ```typescript
2297
+ * const client = new FirefliesClient({
2298
+ * apiKey: '...',
2299
+ * rateLimit: { warningThreshold: 10 }
2300
+ * });
2301
+ *
2302
+ * await client.users.me();
2303
+ * console.log(client.rateLimits);
2304
+ * // { remaining: 59, limit: 60, resetInSeconds: 60, updatedAt: 1706299500000 }
2305
+ * ```
2306
+ */
2307
+ get rateLimits() {
2308
+ return this.graphql.rateLimitState;
2309
+ }
2310
+ };
2311
+ function verifyWebhookSignature(options) {
2312
+ const { payload, signature, secret } = options;
2313
+ if (!signature || !secret) {
2314
+ return false;
2315
+ }
2316
+ const payloadString = typeof payload === "string" ? payload : JSON.stringify(payload);
2317
+ const computed = crypto.createHmac("sha256", secret).update(payloadString).digest("hex");
2318
+ try {
2319
+ return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(computed));
2320
+ } catch {
2321
+ return false;
2322
+ }
2323
+ }
2324
+
2325
+ // src/webhooks/parse.ts
2326
+ var VALID_EVENT_TYPES = ["Transcription completed"];
2327
+ function isValidEventType(value) {
2328
+ return VALID_EVENT_TYPES.includes(value);
2329
+ }
2330
+ function isValidWebhookPayload(payload) {
2331
+ if (!payload || typeof payload !== "object") {
2332
+ return false;
2333
+ }
2334
+ const meetingId = payload["meetingId"];
2335
+ const eventType = payload["eventType"];
2336
+ const clientReferenceId = payload["clientReferenceId"];
2337
+ return typeof meetingId === "string" && typeof eventType === "string" && isValidEventType(eventType) && (clientReferenceId === void 0 || typeof clientReferenceId === "string");
2338
+ }
2339
+ function parseWebhookPayload(payload, options) {
2340
+ if (options?.signature && options?.secret) {
2341
+ const isValid = verifyWebhookSignature({
2342
+ payload,
2343
+ signature: options.signature,
2344
+ secret: options.secret
2345
+ });
2346
+ if (!isValid) {
2347
+ throw new WebhookVerificationError("Invalid webhook signature");
2348
+ }
2349
+ }
2350
+ if (!isValidWebhookPayload(payload)) {
2351
+ throw new WebhookParseError(
2352
+ "Invalid webhook payload: expected meetingId (string), eventType (valid event type), and optional clientReferenceId (string)"
2353
+ );
2354
+ }
2355
+ return payload;
2356
+ }
2357
+
2358
+ // src/middleware/core.ts
2359
+ function validateOptions(options) {
2360
+ if (!options.secret || !options.secret.trim()) {
2361
+ throw new Error("Webhook middleware: secret is required");
2362
+ }
2363
+ }
2364
+ function buildHandlerContext(params) {
2365
+ const { payload, apiKey, transcript } = params;
2366
+ const context = {
2367
+ payload
2368
+ };
2369
+ if (apiKey) {
2370
+ context.client = new FirefliesClient({ apiKey });
2371
+ }
2372
+ if (transcript) {
2373
+ context.transcript = transcript;
2374
+ }
2375
+ return context;
2376
+ }
2377
+ function parseRawBody(rawBody) {
2378
+ return Buffer.isBuffer(rawBody) ? rawBody.toString("utf-8") : rawBody;
2379
+ }
2380
+ function parseAndValidatePayload(rawBody, signature, secret) {
2381
+ if (!signature) {
2382
+ throw new WebhookVerificationError("Missing webhook signature");
2383
+ }
2384
+ const bodyString = parseRawBody(rawBody);
2385
+ let jsonPayload;
2386
+ try {
2387
+ jsonPayload = JSON.parse(bodyString);
2388
+ } catch {
2389
+ throw new WebhookParseError("Invalid JSON in webhook body");
2390
+ }
2391
+ return parseWebhookPayload(jsonPayload, { signature, secret });
2392
+ }
2393
+ async function fetchTranscriptIfEnabled(payload, apiKey, autoFetch) {
2394
+ if (!apiKey || autoFetch === false) {
2395
+ return void 0;
2396
+ }
2397
+ try {
2398
+ const client = new FirefliesClient({ apiKey });
2399
+ return await client.transcripts.get(payload.meetingId);
2400
+ } catch {
2401
+ return void 0;
2402
+ }
2403
+ }
2404
+ async function executeHandlers(context, options) {
2405
+ const { onEvent, onTranscriptionCompleted } = options;
2406
+ if (onEvent) {
2407
+ await onEvent(context);
2408
+ }
2409
+ if (context.payload.eventType === "Transcription completed" && onTranscriptionCompleted) {
2410
+ await onTranscriptionCompleted(context);
2411
+ }
2412
+ }
2413
+ function mapErrorToResponse(error) {
2414
+ if (error instanceof WebhookVerificationError) {
2415
+ return {
2416
+ success: false,
2417
+ statusCode: 401,
2418
+ body: "Invalid webhook signature",
2419
+ error
2420
+ };
2421
+ }
2422
+ if (error instanceof WebhookParseError) {
2423
+ return {
2424
+ success: false,
2425
+ statusCode: 400,
2426
+ body: "Invalid webhook payload",
2427
+ error
2428
+ };
2429
+ }
2430
+ if (error instanceof SyntaxError) {
2431
+ return {
2432
+ success: false,
2433
+ statusCode: 400,
2434
+ body: "Invalid JSON in webhook body",
2435
+ error
2436
+ };
2437
+ }
2438
+ return {
2439
+ success: false,
2440
+ statusCode: 500,
2441
+ body: "Internal server error",
2442
+ error: error instanceof Error ? error : new Error(String(error))
2443
+ };
2444
+ }
2445
+ async function processWebhook(input, options) {
2446
+ const { rawBody, signature } = input;
2447
+ const { secret, apiKey, autoFetch, onError } = options;
2448
+ let parsedPayload;
2449
+ try {
2450
+ parsedPayload = parseAndValidatePayload(rawBody, signature, secret);
2451
+ const transcript = await fetchTranscriptIfEnabled(parsedPayload, apiKey, autoFetch);
2452
+ const context = buildHandlerContext({
2453
+ payload: parsedPayload,
2454
+ apiKey,
2455
+ transcript
2456
+ });
2457
+ await executeHandlers(context, options);
2458
+ return {
2459
+ success: true,
2460
+ statusCode: 200,
2461
+ body: "ok",
2462
+ payload: parsedPayload
2463
+ };
2464
+ } catch (error) {
2465
+ if (onError && error instanceof Error) {
2466
+ await onError(error, parsedPayload);
2467
+ }
2468
+ return mapErrorToResponse(error);
2469
+ }
2470
+ }
2471
+
2472
+ // src/middleware/fastify.ts
2473
+ function createWebhookHandler(options) {
2474
+ validateOptions(options);
2475
+ return async (request, reply) => {
2476
+ const rawBody = request.body;
2477
+ const signature = request.headers["x-hub-signature"];
2478
+ const result = await processWebhook(
2479
+ {
2480
+ rawBody,
2481
+ signature
2482
+ },
2483
+ options
2484
+ );
2485
+ reply.status(result.statusCode).send(result.body);
2486
+ };
2487
+ }
2488
+ var firefliesWebhook = (fastify, options, done) => {
2489
+ validateOptions(options);
2490
+ const { path = "/webhooks/fireflies", ...webhookOptions } = options;
2491
+ fastify.addContentTypeParser("application/json", { parseAs: "buffer" }, (_req, body, done2) => {
2492
+ done2(null, body);
2493
+ });
2494
+ fastify.post(path, createWebhookHandler(webhookOptions));
2495
+ done();
2496
+ };
2497
+
2498
+ exports.createWebhookHandler = createWebhookHandler;
2499
+ exports.firefliesWebhook = firefliesWebhook;
2500
+ //# sourceMappingURL=fastify.cjs.map
2501
+ //# sourceMappingURL=fastify.cjs.map