Is Fireflies: AI meeting notes safe?

Medium risk

Fireflies is medium risk. When a Fireflies recording starts in Meet, the extension creates a meeting record, starts recording, converts captured audio to an MP3 blob, requests a signed upload URL, and uploads it as audio/mp3. No request body captured live.

Fireflies, Incv6.4.1Chrome Web Store
45Risk

AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.

Publishers can request a review.

Findings

SeverityMEDIUM
ClassUNWANTED
TypeUnexpected
CWECWE-200
SourceAI SANDBOX

Fireflies Uploads Meeting Audio to Backend Storage

When a Fireflies recording starts in Meet, the extension creates a meeting record, starts recording, converts captured audio to an MP3 blob, requests a signed upload URL, and uploads it as audio/mp3.

No request body captured live.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You start a Fireflies recording from a Google Meet page.

The extension did this

The extension records meeting audio, converts it to MP3, and uploads it to Fireflies storage.

It also sends meeting setup metadata through the Fireflies GraphQL service.

02EvidenceFIELD TABLE
Recording fields handled by the upload path
FieldValueWhy it matters
Meeting URL
https://meet.google.com/abc-defg-hijLinks the recording to the Meet session you were in.
Meeting title
Weekly Product SyncAdds human-readable meeting context to the recording record.
Meeting identifier
meet_7b2f1c9e4a8d4c2fTies the uploaded audio file to one Fireflies meeting record.
Audio MIME type
audio/mp3Labels the uploaded recording as an MP3 audio file.
Audio size
1843200Reveals how large the captured meeting audio file is.
Audio device list
{"deviceId":"default","label":"Built-in Microphone","groupId":"8f24cce7a6"}Describes the microphone inputs available during the recording setup.
03EvidenceNETWORK CAPTURE
Captured request
POSThttps://gateway.fireflies.ai/graphql
Returns a signed upload URL for the meeting audio file.
Headers
Content-Typeapplication/json
AuthorizationBearer <redacted>
04EvidenceNETWORK CAPTURE
Captured request
PUThttps://media-storage.firefliesapp.com
Successful upload is treated as HTTP 200 by the upload helper.
Headers
Content-Typeaudio/mp3
05EvidenceCODE COMPARE
The code that does this

Meet recording creates metadata and prepares the audio file

What it actually does
Manual recording button dispatches START_MANUAL_RECORDINGgmeetUserPanel.12259ce7.js
    var manualStartRecordingProps = {
        title: meetTitle,
        url: window.location.href,
        isLoading: !isGrowthBookReady,
        onStart: function onStart() {
            return relayMessage(_misc.Msg.START_MANUAL_RECORDING);
        },
        onInviteSuccess: closePopup,
        canInviteNotetaker: invitedNotetaker === false,
        location: "banner",
        autoCaptureEnabled: true
    };
Recorder creates a meeting and starts recordinggmeetRecorder.c38e228a.js
    var startRecording = /*#__PURE__*/ function() {
        var _ref11 = (0, _asyncToGenerator2["default"])(/*#__PURE__*/ (0, _regenerator2["default"])().m(function _callee9() {
            var isAutoCaptured, shouldCheckFred, notificationShown, _Text$notifications$C, heading, text, subHeading, meeting, markRecordingOnPage, resp, meetingInstance, audioDevices, allDevices, _args9 = arguments, _t3, _t4, _t5, _t6, _t7, _t8, _t9, _t0, _t1, _t10;
            return (0, _regenerator2["default"])().w(function(_context9) {
                while(true)switch(_context9.p = _context9.n){
                    case 0:
                        isAutoCaptured = _args9.length > 0 && _args9[0] !== undefined ? _args9[0] : false;
                        shouldCheckFred = _args9.length > 1 && _args9[1] !== undefined ? _args9[1] : false;
                        if (!isCompanionMode()) {
                            _context9.n = 7;
                            break;
                        }
                        notificationShown = false; // Show a message to the user about companion mode limitations
                        _context9.p = 1;
                        _Text$notifications$C = _localization["default"].notifications.CompanionModeDetected, heading = _Text$notifications$C.title, text = _Text$notifications$C.message, subHeading = _Text$notifications$C.subHeading;
                        showNotification({
                            type: "screen-notification",
                            heading: heading,
                            subHeading: subHeading,
                            text: text,
                            ttlSeconds: 30
                        });
                        notificationShown = true;
                        // Log additional details about the meeting when companion mode is detected
                        _context9.n = 2;
                        return waitForExtensionId();
                    case 2:
                        _t3 = _misc.sendToBackground;
                        _t4 = extensionId.current;
                        _t5 = CONTENT_SCRIPT_ID;
                        _t6 = "Companion mode detected during recording attempt: ".concat(window.location.href, ", Meeting title: ");
                        _context9.n = 3;
                        return getMeetingTitle();
                    case 3:
                        _t7 = _t6.concat.call(_t6, _context9.v, ", isAutoCaptured: ").concat(isAutoCaptured);
                        _t8 = {
                            script: _t5,
                            errorMessage: _t7
                        };
                        _context9.n = 4;
                        return _t3(_t4, {
                            name: "capture-content-script-error",
                            body: _t8
                        });
                    case 4:
                        _context9.n = 6;
                        break;
                    case 5:
                        _context9.p = 5;
                        _t9 = _context9.v;
                        if (!notificationShown) {
                            // Fallback in case localization is not available
                            showNotification({
                                type: "screen-notification",
                                heading: "Audio Recording Not Available",
                                subHeading: "Companion Mode Detected",
                                text: "You're in Google Meet's companion mode, which doesn't allow microphone access. The recording may result in blank audio.",
                                ttlSeconds: 30
                            });
                            notificationShown = true;
                        }
                    case 6:
                        return _context9.a(2);
                    case 7:
                        _context9.n = 8;
                        return getMeetingTitle();
                    case 8:
                        _t0 = _context9.v;
                        _t1 = window.location.href;
                        meeting = {
                            title: _t0,
                            url: _t1
                        };
                        markRecordingOnPage = function markRecordingOnPage() {
                            hasRecordedOnPage.current = true;
                        }; // Add listener if auto capture allowed and fred toggle is disabled
                        if (isAutoCaptured && shouldCheckFred) removeUserJoinListener.current = (0, _misc.sendStenographerCmd)("getUser", undefined, handleGmeetAttendee);
                        _context9.n = 9;
                        return waitForExtensionId();
                    case 9:
                        _context9.n = 10;
                        return (0, _misc.sendToBackground)(extensionId.current, {
                            name: "create-meeting",
                            body: {
                                meeting: meeting,
                                isAutoCaptured: isAutoCaptured,
                                createdBy: "google-meet-".concat(isAutoCaptured ? "auto" : "manual", "-capture")
                            }
                        });
                    case 10:
                        resp = _context9.v;
                        meetingInstance = resp.data;
                        if (!meetingInstance) {
                            _context9.n = 16;
                            break;
                        }
                        audioDevices = [];
                        _context9.p = 11;
                        _context9.n = 12;
                        return navigator.mediaDevices.enumerateDevices();
                    case 12:
                        allDevices = _context9.v;
                        audioDevices = allDevices.filter(function(d) {
                            return d.kind === "audioinput";
                        }).map(function(d) {
                            return {
                                deviceId: d.deviceId,
                                label: d.label || "",
                                groupId: d.groupId
                            };
                        });
                        _context9.n = 14;
                        break;
                    case 13:
                        _context9.p = 13;
                        _t10 = _context9.v;
                    case 14:
                        _context9.n = 15;
                        return waitForExtensionId();
                    case 15:
                        _context9.n = 16;
                        return (0, _misc.sendToBackground)(extensionId.current, {
                            name: "start-recording",
                            body: {
                                id: meetingInstance.id,
                                isAutoCaptured: isAutoCaptured,
                                audioDevices: audioDevices,
                                recordingType: recordingType.current
                            }
                        }).then(function(response) {
                            if (!(response !== null && response !== void 0 && response.status)) return;
                            var shouldToggleCaptions = !hasRecordedOnPage.current;
                            markRecordingOnPage();
                            if (shouldToggleCaptions) toggleCCButton();
                            setMeeting(meetingInstance);
                            meetingIdRef.current = meetingInstance.id;
                            setMeetingState("STARTED");
                            showNotification();
                        });
                    case 16:
                        return _context9.a(2);
                }
            }, _callee9, null, [
                [
                    11,
                    13
                ],
                [
                    1,
                    5
                ]
            ]);
        }));
        return function startRecording() {
            return _ref11.apply(this, arguments);
        };
    }();
Offscreen page converts captured audio into an MP3 bloboffscreen.5a38bb83.js
                    meetingAudio = new Blob(validAudioRecords.map(function(record) {
                        return record.blob;
                    }), {
                        type: "audio/webm"
                    });
                    if (!(!meetingAudio || meetingAudio.size === 0)) {
                        _context9.n = 10;
                        break;
                    }
                    return _context9.a(2, handleTranscodingError((0, _objectSpread2["default"])((0, _objectSpread2["default"])({}, transcodeErrorParams), {}, {
                        state: _types.MeetingState.NOT_ENOUGH_AUDIO_SEGMENT,
                        processed: true
                    })));
                case 10:
                    _context9.p = 10;
                    if (ffmpegInstance) {
                        _context9.n = 14;
                        break;
                    }
                    ffmpegInstance = new _ffmpeg2.FFmpeg((_chrome$runtime = chrome.runtime) === null || _chrome$runtime === void 0 ? void 0 : _chrome$runtime.getURL("assets/ffmpeg/ffmpeg-core.wasm"), (_chrome$runtime2 = chrome.runtime) === null || _chrome$runtime2 === void 0 ? void 0 : _chrome$runtime2.getURL("assets/ffmpeg/ffmpeg-core.js"), (_chrome$runtime3 = chrome.runtime) === null || _chrome$runtime3 === void 0 ? void 0 : _chrome$runtime3.getURL("assets/ffmpeg/ffmpeg-core.worker.js"));
                    _context9.p = 11;
                    _context9.n = 12;
                    return ffmpegInstance.load();
                case 12:
                    _context9.n = 14;
                    break;
                case 13:
                    _context9.p = 13;
                    _t6 = _context9.v;
                    throw new Error("FFMPEG_ASSET_UNAVAILABLE: ".concat(_t6.message));
                case 14:
                    _context9.p = 14;
                    _context9.n = 15;
                    return (0, _ffmpeg.fetchFile)(meetingAudio);
                case 15:
                    audio = _context9.v;
                    _context9.n = 17;
                    break;
                case 16:
                    _context9.p = 16;
                    _t7 = _context9.v;
                    throw new Error("AUDIO_BLOB_READ_FAILED: ".concat(_t7.message));
                case 17:
                    _offscreen.OffscreenLogger.info("transcoding audio for meeting ".concat(meetingId, " with ").concat(tries, " tries"));
                    _offscreen.OffscreenLogger.info("transcoding audio with ".concat(meetingAudio.size, " bytes"));
                    _context9.n = 18;
                    return ffmpegInstance.addFile("audio_".concat(meetingId, ".mp3"), audio);
                case 18:
                    _context9.n = 19;
                    return ffmpegInstance.run("-i", "audio_".concat(meetingId, ".mp3"), "output_".concat(meetingId, ".mp3"));
                case 19:
                    _context9.n = 20;
                    return ffmpegInstance.getFile("output_".concat(meetingId, ".mp3"));
                case 20:
                    file = _context9.v;
                    blob = new Blob([
                        file
                    ], {
                        type: "audio/mp3"
                    });
                    if (!(!blob || blob.size === 0)) {
                        _context9.n = 21;
                        break;
                    }
                    throw new Error("meeting blob is not created properly");
                case 21:
                    _context9.p = 21;
                    _context9.n = 22;
                    return databaseService.db.meetings.update(meetingId, {
                        audio_file: blob,
                        has_audio: 1
                    });
06EvidenceCODE COMPARE
The code that does this

The service worker requests a signed URL and uploads the MP3

What it actually does
GraphQL startUpload mutation returns the signed upload URLbackground.cdd50369.js
var START_UPLOAD_MUTATION = (0, _client.gql)(_templateObject4 || (_templateObject4 = (0, _taggedTemplateLiteral2["default"])([
    "\n  mutation startUpload($meetingId: String!, $mimeType: String!, $size: Float!, $client: String!) {\n    startUpload(meetingId: $meetingId, mimeType: $mimeType, size: $size, client: $client) {\n      uploadUrl\n      mediaUrl\n    }\n  }\n"
])));
function startUploadViaGraphQL(_x7, _x8) {
    return _startUploadViaGraphQL.apply(this, arguments);
}
function _startUploadViaGraphQL() {
    _startUploadViaGraphQL = (0, _asyncToGenerator2["default"])(/*#__PURE__*/ (0, _regenerator2["default"])().m(function _callee4(token, input) {
        var client, _yield$client$mutate4, data, uploadUrl, _t4;
        return (0, _regenerator2["default"])().w(function(_context4) {
            while(true)switch(_context4.p = _context4.n){
                case 0:
                    getGraphqlLog().info("startUpload | meetingId=".concat(input.meetingId, " | mimeType=").concat(input.mimeType, " | size=").concat(input.size));
                    client = (0, _appollo.getApolloClient)(token);
                    _context4.p = 1;
                    _context4.n = 2;
                    return client.mutate({
                        mutation: START_UPLOAD_MUTATION,
                        variables: input
                    });
                case 2:
                    _yield$client$mutate4 = _context4.v;
                    data = _yield$client$mutate4.data;
                    uploadUrl = data.startUpload.uploadUrl;
                    getGraphqlLog().info("startUpload success | meetingId=".concat(input.meetingId, " | hasUrl=").concat(!!uploadUrl));
                    return _context4.a(2, uploadUrl);
                case 3:
                    _context4.p = 3;
                    _t4 = _context4.v;
                    getGraphqlLog().error("startUpload failed | meetingId=".concat(input.meetingId, " | size=").concat(input.size, " | error=").concat(_t4.message));
                    throw _t4;
                case 4:
                    return _context4.a(2);
            }
        }, _callee4, null, [
            [
                1,
                3
            ]
        ]);
    }));
    return _startUploadViaGraphQL.apply(this, arguments);
}
Service worker obtains a signed URL and uploads the audio blobbackground.cdd50369.js
var UploadAudioTask = exports.UploadAudioTask = /*#__PURE__*/ function(_Task) {
    function UploadAudioTask() {
        (0, _classCallCheck2["default"])(this, UploadAudioTask);
        return (0, _callSuper2["default"])(this, UploadAudioTask, [
            "UploadAudioTask"
        ]);
    }
    (0, _inherits2["default"])(UploadAudioTask, _Task);
    return (0, _createClass2["default"])(UploadAudioTask, [
        {
            key: "do",
            value: function() {
                var _do2 = (0, _asyncToGenerator2["default"])(/*#__PURE__*/ (0, _regenerator2["default"])().m(function _callee2(params) {
                    var startTime, meeting, report, errorMessage, accessToken, audio, meetingId, uploadUrl, err, uploadStartTime, uploadResult, _uploadResult$error, _uploadResult$error2, _uploadResult$error3, uploadEndTime, endTime, _t2;
                    return (0, _regenerator2["default"])().w(function(_context2) {
                        while(true)switch(_context2.p = _context2.n){
                            case 0:
                                startTime = Date.now();
                                _context2.n = 1;
                                return _singletons.meetingService.getMeeting(params.id);
                            case 1:
                                meeting = _context2.v;
                                report = _logger.LogReport["with"]("Meeting - processing report", params.id);
                                if (meeting.parse_id) {
                                    _context2.n = 2;
                                    break;
                                }
                                errorMessage = "Meeting audio cannot be uploaded because parse_id is missing";
                                report.error(errorMessage);
                                throw new Error(errorMessage);
                            case 2:
                                if (!meeting.has_uploaded_audio) {
                                    _context2.n = 4;
                                    break;
                                }
                                _context2.n = 3;
                                return _singletons.meetingService.update(meeting.id, {
                                    upload_progress: 60
                                });
                            case 3:
                                return _context2.a(2, {
                                    id: params.id
                                });
                            case 4:
                                _context2.n = 5;
                                return _singletons.authService.getAccessToken();
                            case 5:
                                accessToken = _context2.v;
                                if (accessToken) {
                                    _context2.n = 6;
                                    break;
                                }
                                console.error("no token found");
                                return _context2.a(2, {});
                            case 6:
                                _context2.n = 7;
                                return _singletons.meetingService.update(meeting.id, {
                                    upload_progress: 10
                                });
                            case 7:
                                audio = meeting.audio_file;
                                if (audio) {
                                    _context2.n = 8;
                                    break;
                                }
                                report.error("audio file not found for meeting ".concat(params.id));
                                throw new Error(_misc.transcodeError);
                            case 8:
                                meetingId = meeting.parse_id; // Step 1: Get signed URL via GraphQL startUpload mutation
                                _context2.p = 9;
                                _context2.n = 10;
                                return (0, _mediaStorageGraphql.startUploadViaGraphQL)(accessToken, {
                                    meetingId: meetingId,
                                    mimeType: "audio/mp3",
                                    size: audio.size,
                                    client: _constants.configStore.getConfig("APP_CODE")
                                });
                            case 10:
                                uploadUrl = _context2.v;
                                if (uploadUrl) {
                                    _context2.n = 11;
                                    break;
                                }
                                throw new Error("startUpload response missing uploadUrl");
                            case 11:
                                if (uploadUrl.startsWith("https://")) {
                                    _context2.n = 12;
                                    break;
                                }
                                throw new Error("Signed URL must use HTTPS protocol");
                            case 12:
                                report.debug("Obtained signed URL for upload: ".concat(uploadUrl.substring(0, 50), "..."));
                                _context2.n = 14;
                                break;
                            case 13:
                                _context2.p = 13;
                                _t2 = _context2.v;
                                err = _t2;
                                throw new _errors.TaskWarning("signed-url-request-failed", {
                                    message: "Failed to obtain signed URL for audio upload",
                                    id: params.id,
                                    meta: {
                                        parseId: meeting.parse_id,
                                        errorMessage: err.message
                                    }
                                });
                            case 14:
                                // Step 2: Upload audio to cloud storage using signed URL
                                uploadStartTime = Date.now();
                                _context2.n = 15;
                                return (0, _uploadWithRetry.uploadWithRetry)({
                                    url: uploadUrl,
                                    blob: audio,
                                    contentType: "audio/mp3",
                                    timeout: _uploadWithRetry.AUDIO_UPLOAD_CONFIG.timeout,
                                    retryHandler: _uploadWithRetry.AUDIO_UPLOAD_CONFIG.retryHandler,
                                    onProgress: function() {
                                        var _onProgress = (0, _asyncToGenerator2["default"])(/*#__PURE__*/ (0, _regenerator2["default"])().m(function _callee(progress) {
                                            var taskProgress, _t;
                                            return (0, _regenerator2["default"])().w(function(_context) {
                                                while(true)switch(_context.p = _context.n){
                                                    case 0:
                                                        _context.p = 0;
                                                        // Note: Service workers only have fetch API which doesn't support upload progress
                                                        // onProgress is called at 0% (start) and 100% (complete) only
                                                        // Map to task progress: 10% (start) -> 55% (complete) -> 60% (metadata updated)
                                                        taskProgress = 10 + Math.floor(progress.percentage * 0.45);
                                                        _context.n = 1;
                                                        return _singletons.meetingService.update(meeting.id, {
                                                            upload_progress: taskProgress
                                                        });
                                                    case 1:
                                                        if (progress.percentage === 0) report.debug("Starting upload: ".concat(audio.size, " bytes"));
                                                        else if (progress.percentage === 100) report.debug("Upload complete: ".concat(audio.size, " bytes in ").concat(Date.now() - uploadStartTime, "ms"));
                                                        _context.n = 3;
                                                        break;
                                                    case 2:
                                                        _context.p = 2;
                                                        _t = _context.v;
                                                        report.error("Failed to update upload progress: ".concat(_t.message || _t, ". ") + "Context: meetingId=".concat(meeting.id, ", audioSize=").concat(audio.size, ", ") + "uploadStartTime=".concat(uploadStartTime, ", progress=").concat(progress.percentage, "%"));
                                                    // Swallow error to prevent rejecting the upload operation
                                                    case 3:
                                                        return _context.a(2);
                                                }
                                            }, _callee, null, [
                                                [
                                                    0,
                                                    2
                                                ]
                                            ]);
                                        }));
                                        function onProgress(_x2) {
                                            return _onProgress.apply(this, arguments);
                                        }
                                        return onProgress;
                                    }()
                                });
                            case 15:
                                uploadResult = _context2.v;
                                if (uploadResult.success) {
                                    _context2.n = 16;
                                    break;
                                }
                                uploadEndTime = Date.now();
                                throw new _errors.TaskWarning("audio-upload-to-storage-failed", {
                                    message: "Failed to upload audio to cloud storage",
                                    id: params.id,
                                    meta: {
                                        parseId: meeting.parse_id,
                                        uploadUrl: uploadUrl.substring(0, 100),
                                        statusCode: uploadResult.statusCode,
                                        errorType: (_uploadResult$error = uploadResult.error) === null || _uploadResult$error === void 0 ? void 0 : _uploadResult$error.type,
                                        errorMessage: (_uploadResult$error2 = uploadResult.error) === null || _uploadResult$error2 === void 0 ? void 0 : _uploadResult$error2.message,
                                        audioSize: audio.size,
                                        uploadDuration: uploadEndTime - uploadStartTime,
                                        attempt: (_uploadResult$error3 = uploadResult.error) === null || _uploadResult$error3 === void 0 ? void 0 : _uploadResult$error3.context.attempt
                                    }
                                });
                            case 16:
                                report.info("Audio upload succeeded: ".concat(audio.size, " bytes uploaded in ").concat(Date.now() - uploadStartTime, "ms"));
                                endTime = Date.now();
                                (0, _analytics.analyticsTrack)(_analytics2.SegmentAnalyticsEvents.UPLOAD_MP3_AUDIO, (0, _objectSpread2["default"])((0, _objectSpread2["default"])({}, meeting.meeting_analytics || {}), {}, {
                                    uploadTime: endTime - startTime,
                                    audioChunksCount: meeting.audio_segments,
                                    sizeMp3: audio.size,
                                    parseId: meeting.parse_id
                                }));
                                _context2.n = 17;
                                return _singletons.meetingService.update(params.id, {
                                    has_uploaded_audio: 1,
                                    upload_progress: 60
                                });
                            case 17:
                                _offscreen["default"].send("remove-audio-chunks", meeting.id);
                                return _context2.a(2, {
                                    id: params.id
                                });
                        }
                    }, _callee2, null, [
                        [
                            9,
                            13
                        ]
                    ]);
                }));
                function _do(_x) {
                    return _do2.apply(this, arguments);
                }
                return _do;
            }()
        }
    ]);
}(_task.Task);
Signed URL upload uses PUT with the audio content typebackground.cdd50369.js
 */ function uploadWithRetry(_x) {
    return _uploadWithRetry.apply(this, arguments);
}
/**
 * Upload configuration for audio files specifically
 * Pre-configured with audio-appropriate timeout and retry settings
 */ function _uploadWithRetry() {
    _uploadWithRetry = (0, _asyncToGenerator2["default"])(/*#__PURE__*/ (0, _regenerator2["default"])().m(function _callee(options) {
        var url, blob, contentType, _options$timeout, timeout, _options$retryHandler, retryHandler, onProgress, api, networkError, _t;
        return (0, _regenerator2["default"])().w(function(_context) {
            while(true)switch(_context.p = _context.n){
                case 0:
                    url = options.url, blob = options.blob, contentType = options.contentType, _options$timeout = options.timeout, timeout = _options$timeout === void 0 ? 1800000 : _options$timeout, _options$retryHandler = options.retryHandler, retryHandler = _options$retryHandler === void 0 ? new _retry.RetryHandler((0, _objectSpread2["default"])((0, _objectSpread2["default"])({}, _retry.DEFAULT_RETRY_CONFIG), {}, {
                        maxRetries: 3,
                        // More conservative for uploads
                        baseDelay: 1000,
                        maxDelay: 10000
                    })) : _options$retryHandler, onProgress = options.onProgress; // Report upload start (0%)
                    if (!onProgress) {
                        _context.n = 1;
                        break;
                    }
                    _context.n = 1;
                    return onProgress({
                        loaded: 0,
                        total: blob.size,
                        percentage: 0
                    });
                case 1:
                    // Create FFApi instance without baseUrl (we have full URL) and without auth (signed URLs are pre-authorized)
                    api = new _ffApi.FFApi("", undefined, {
                        timeout: timeout,
                        retryHandler: retryHandler
                    });
                    _context.p = 2;
                    _context.n = 3;
                    return api.putBlob(url, blob, {
                        "Content-Type": contentType
                    }, true // withRetry
                    );
                case 3:
                    if (!onProgress) {
                        _context.n = 4;
                        break;
                    }
                    _context.n = 4;
                    return onProgress({
                        loaded: blob.size,
                        total: blob.size,
                        percentage: 100
                    });
                case 4:
                    return _context.a(2, {
                        success: true,
                        statusCode: 200 // Successful upload (2xx status)
                    });
                case 5:
                    _context.p = 5;
                    _t = _context.v;
                    // FFApi throws NetworkError on failure
                    networkError = _t instanceof _errors.NetworkError ? _t : new _errors.NetworkError(_t.message || "Upload failed", _errors.NetworkErrorType.UNKNOWN_ERROR, {
                        url: url,
                        method: "PUT",
                        attempt: retryHandler.currentAttempt || 1,
                        maxRetries: retryHandler.maxRetries,
                        timestamp: Date.now(),
                        isOnline: navigator.onLine
                    });
                    return _context.a(2, {
                        success: false,
                        statusCode: networkError.statusCode,
                        error: networkError
                    });
            }
        }, _callee, null, [
            [
                2,
                5
            ]
        ]);
    }));
    return _uploadWithRetry.apply(this, arguments);
}
07EvidenceTHIRD PARTY LIST
Fireflies services contacted by the recording flow
  • gateway.fireflies.ai

    Receives GraphQL mutations that create meeting records and return signed upload URLs.

  • media-storage.firefliesapp.com

    Receives the MP3 meeting audio blob through the signed upload URL.

  • calendar.firefliesapp.com

    Configured endpoint for calendar event access around meeting workflows.

  • user-service-rest.fireflies.ai

    Configured endpoint for Fireflies user authentication and profile requests.

Updated 17 September 2026meimoidfecamngeoanhnpdjjdcefoldn