@@ -12,14 +12,11 @@ import {
1212 TASK_EXTRACTION_PROMPT ,
1313} from "./transcription-prompts.js" ;
1414
15- // Polyfill File for Node.js compatibility
1615let FileConstructor : typeof File ;
1716try {
18- // Try importing from node:buffer (Node 20+)
1917 const { File : NodeFile } = await import ( "node:buffer" ) ;
2018 FileConstructor = NodeFile as typeof File ;
2119} catch {
22- // Fallback for Node < 20
2320 FileConstructor = class File extends Blob {
2421 name : string ;
2522 lastModified : number ;
@@ -44,7 +41,6 @@ interface RecordingSession {
4441const activeRecordings = new Map < string , RecordingSession > ( ) ;
4542const recordingsDir = path . join ( app . getPath ( "userData" ) , "recordings" ) ;
4643
47- // Ensure recordings directory exists
4844if ( ! fs . existsSync ( recordingsDir ) ) {
4945 fs . mkdirSync ( recordingsDir , { recursive : true } ) ;
5046}
@@ -53,21 +49,16 @@ if (!fs.existsSync(recordingsDir)) {
5349 * Validates a recording ID to prevent path traversal attacks
5450 */
5551function validateRecordingId ( recordingId : string ) : boolean {
56- // Only allow alphanumeric characters, dots, hyphens, and underscores
5752 const safePattern = / ^ [ a - z A - Z 0 - 9 . _ - ] + $ / ;
5853 if ( ! safePattern . test ( recordingId ) ) {
5954 return false ;
6055 }
6156
62- // Ensure the resolved path stays within the recordings directory
6357 const resolvedPath = path . resolve ( path . join ( recordingsDir , recordingId ) ) ;
6458 const recordingsDirResolved = path . resolve ( recordingsDir ) ;
6559 return resolvedPath . startsWith ( recordingsDirResolved + path . sep ) ;
6660}
6761
68- /**
69- * Generate a summary title for a transcript using GPT with structured output
70- */
7162async function generateTranscriptSummary (
7263 transcriptText : string ,
7364 openaiApiKey : string ,
@@ -100,9 +91,6 @@ async function generateTranscriptSummary(
10091 }
10192}
10293
103- /**
104- * Extract actionable tasks from a transcript using GPT with structured output
105- */
10694async function extractTasksFromTranscript (
10795 transcriptText : string ,
10896 openaiApiKey : string ,
@@ -146,12 +134,11 @@ function safeLog(...args: unknown[]): void {
146134 try {
147135 console . log ( ...args ) ;
148136 } catch {
149- // Ignore logging errors (e.g., write EIO during startup)
137+ // Ignore logging errors
150138 }
151139}
152140
153141export function registerRecordingIpc ( ) : void {
154- // Desktop capturer for system audio
155142 ipcMain . handle (
156143 "desktop-capturer:get-sources" ,
157144 async ( _event , options : { types : ( "screen" | "window" ) [ ] } ) => {
@@ -194,16 +181,13 @@ export function registerRecordingIpc(): void {
194181 throw new Error ( "Recording session not found" ) ;
195182 }
196183
197- // Save the recording
198184 const filename = `recording-${ session . startTime . toISOString ( ) . replace ( / [: .] / g, "-" ) } .webm` ;
199185 const filePath = path . join ( recordingsDir , filename ) ;
200186 const metadataPath = path . join ( recordingsDir , `${ filename } .json` ) ;
201187
202- // Save the audio data
203188 const buffer = Buffer . from ( audioData ) ;
204189 fs . writeFileSync ( filePath , buffer ) ;
205190
206- // Save metadata
207191 const metadata = {
208192 duration,
209193 created_at : session . startTime . toISOString ( ) ,
@@ -243,7 +227,6 @@ export function registerRecordingIpc(): void {
243227 let createdAt = new Date ( ) . toISOString ( ) ;
244228 let transcription : Recording [ "transcription" ] ;
245229
246- // Try to read metadata
247230 if ( fs . existsSync ( metadataPath ) ) {
248231 try {
249232 const metadataContent = fs . readFileSync ( metadataPath , "utf-8" ) ;
@@ -278,6 +261,13 @@ export function registerRecordingIpc(): void {
278261 }
279262
280263 const filePath = path . join ( recordingsDir , recordingId ) ;
264+
265+ const resolvedPath = fs . realpathSync . native ( filePath ) ;
266+ const recordingsDirResolved = fs . realpathSync . native ( recordingsDir ) ;
267+ if ( ! resolvedPath . startsWith ( recordingsDirResolved ) ) {
268+ throw new Error ( "Invalid recording path" ) ;
269+ }
270+
281271 const metadataPath = path . join ( recordingsDir , `${ recordingId } .json` ) ;
282272
283273 let deleted = false ;
@@ -305,6 +295,12 @@ export function registerRecordingIpc(): void {
305295 throw new Error ( "Recording file not found" ) ;
306296 }
307297
298+ const resolvedPath = fs . realpathSync . native ( filePath ) ;
299+ const recordingsDirResolved = fs . realpathSync . native ( recordingsDir ) ;
300+ if ( ! resolvedPath . startsWith ( recordingsDirResolved ) ) {
301+ throw new Error ( "Invalid recording path" ) ;
302+ }
303+
308304 const buffer = fs . readFileSync ( filePath ) ;
309305 return buffer ;
310306 } ) ;
@@ -323,7 +319,12 @@ export function registerRecordingIpc(): void {
323319 throw new Error ( "Recording file not found" ) ;
324320 }
325321
326- // Update metadata to show processing
322+ const resolvedPath = fs . realpathSync . native ( filePath ) ;
323+ const recordingsDirResolved = fs . realpathSync . native ( recordingsDir ) ;
324+ if ( ! resolvedPath . startsWith ( recordingsDirResolved ) ) {
325+ throw new Error ( "Invalid recording path" ) ;
326+ }
327+
327328 let metadata : Record < string , unknown > = { } ;
328329 if ( fs . existsSync ( metadataPath ) ) {
329330 metadata = JSON . parse ( fs . readFileSync ( metadataPath , "utf-8" ) ) ;
@@ -338,16 +339,14 @@ export function registerRecordingIpc(): void {
338339 try {
339340 const openai = createOpenAI ( { apiKey : openaiApiKey } ) ;
340341
341- // Read the file
342342 const audio = await readFile ( filePath ) ;
343- const maxSize = 25 * 1024 * 1024 ; // 25 MB limit
343+ const maxSize = 25 * 1024 * 1024 ;
344344 const fileSize = audio . length ;
345345
346346 safeLog (
347347 `[Transcription] Starting (${ ( fileSize / 1024 / 1024 ) . toFixed ( 2 ) } MB)` ,
348348 ) ;
349349
350- // Check file size
351350 if ( fileSize > maxSize ) {
352351 throw new Error (
353352 `Recording file is too large (${ ( fileSize / 1024 / 1024 ) . toFixed ( 1 ) } MB). ` +
@@ -356,7 +355,6 @@ export function registerRecordingIpc(): void {
356355 ) ;
357356 }
358357
359- // Call OpenAI transcription with detailed output
360358 const result = await transcribe ( {
361359 model : openai . transcription ( "gpt-4o-mini-transcribe" ) ,
362360 audio,
@@ -366,13 +364,11 @@ export function registerRecordingIpc(): void {
366364
367365 const fullTranscriptText = result . text ;
368366
369- // Generate summary title
370367 const summaryTitle = await generateTranscriptSummary (
371368 fullTranscriptText ,
372369 openaiApiKey ,
373370 ) ;
374371
375- // Extract actionable tasks using GPT
376372 const extractedTasks = await extractTasksFromTranscript (
377373 fullTranscriptText ,
378374 openaiApiKey ,
@@ -382,7 +378,6 @@ export function registerRecordingIpc(): void {
382378 `[Transcription] Complete - ${ extractedTasks . length } tasks extracted` ,
383379 ) ;
384380
385- // Update metadata with transcription, summary, and extracted tasks
386381 metadata . transcription = {
387382 status : "completed" ,
388383 text : fullTranscriptText ,
@@ -400,7 +395,6 @@ export function registerRecordingIpc(): void {
400395 } catch ( error ) {
401396 console . error ( "[Transcription] Error:" , error ) ;
402397
403- // Update metadata with error
404398 metadata . transcription = {
405399 status : "error" ,
406400 text : "" ,
0 commit comments