From 6e7342d16c0071dbc09fcdcc4dc0e13b475a4141 Mon Sep 17 00:00:00 2001 From: webadderall <131426131+webadderall@users.noreply.github.com> Date: Mon, 15 Jun 2026 13:43:08 +1000 Subject: [PATCH] fix(projects): tighten import and document project files --- electron-builder.json5 | 181 ++++++++++-------- electron/ipc/project/manager.test.ts | 39 +++- electron/ipc/project/manager.ts | 30 +++ .../video-editor/projectDirtyState.test.ts | 54 ++++++ 4 files changed, 218 insertions(+), 86 deletions(-) diff --git a/electron-builder.json5 b/electron-builder.json5 index 1aaa56af..a333a872 100644 --- a/electron-builder.json5 +++ b/electron-builder.json5 @@ -1,22 +1,22 @@ -// @see - https://www.electron.build/configuration/configuration -{ - "$schema": "https://raw.githubusercontent.com/electron-userland/electron-builder/master/packages/app-builder-lib/scheme.json", - "appId": "dev.recordly.app", - "electronUpdaterCompatibility": ">=2.16", - "asar": true, - "asarUnpack": [ +// @see - https://www.electron.build/configuration/configuration +{ + "$schema": "https://raw.githubusercontent.com/electron-userland/electron-builder/master/packages/app-builder-lib/scheme.json", + "appId": "dev.recordly.app", + "electronUpdaterCompatibility": ">=2.16", + "asar": true, + "asarUnpack": [ "node_modules/ffmpeg-static/**", "node_modules/ffprobe-static/**", "node_modules/uiohook-napi/**", - "electron/native/**" - ], - "productName": "Recordly", - "npmRebuild": true, - "buildDependenciesFromSource": true, - "compression": "normal", - "directories": { - "output": "release" - }, + "electron/native/**" + ], + "productName": "Recordly", + "npmRebuild": true, + "buildDependenciesFromSource": true, + "compression": "normal", + "directories": { + "output": "release" + }, "files": [ "dist", "dist-electron", @@ -27,67 +27,88 @@ "!electron/native/**/build/**", "!*.png", "!preview*.png", - "!*.md", - "!README.md", - "!CONTRIBUTING.md", - "!LICENSE" - ], - "extraResources": [ - { - "from": "public/wallpapers", - "to": "assets/wallpapers" - } - ], - "publish": [ - { - "provider": "github", - "owner": "webadderall", - "repo": "Recordly", - "tagNamePrefix": "v", - "publishAutoUpdate": true - } - ], - - "mac": { - "hardenedRuntime": true, - "entitlements": "build/entitlements.mac.plist", - "entitlementsInherit": "build/entitlements.mac.inherit.plist", - "target": [ - { - "target": "dmg", - "arch": ["x64", "arm64"] - }, - { - "target": "zip", - "arch": ["x64", "arm64"] - } - ], - "icon": "icons/icons/mac/icon.icns", - "artifactName": "${productName}-${arch}.${ext}", - "extendInfo": { - "NSAudioCaptureUsageDescription": "Recordly needs audio capture permission to record system audio.", - "NSCameraUsageDescription": "Recordly needs camera access to record webcam video.", - "NSMicrophoneUsageDescription": "Recordly needs microphone access to record voice audio.", - "NSCameraUseContinuityCameraDeviceType": true, - "com.apple.security.device.audio-input": true - } - }, - "linux": { - "target": [ - "AppImage" - ], - "icon": "icons/icons/png", - "artifactName": "${productName}-linux-x64.${ext}", - "category": "AudioVideo" - }, - "win": { - "target": [ - "nsis" - ], - "icon": "icons/icons/win/icon.ico" - , - "executableName": "Recordly", - "artifactName": "${productName}-windows-${arch}.${ext}" - } -} - + "!*.md", + "!README.md", + "!CONTRIBUTING.md", + "!LICENSE" + ], + "extraResources": [ + { + "from": "public/wallpapers", + "to": "assets/wallpapers" + } + ], + "publish": [ + { + "provider": "github", + "owner": "webadderall", + "repo": "Recordly", + "tagNamePrefix": "v", + "publishAutoUpdate": true + } + ], + + "mac": { + "hardenedRuntime": true, + "entitlements": "build/entitlements.mac.plist", + "entitlementsInherit": "build/entitlements.mac.inherit.plist", + "target": [ + { + "target": "dmg", + "arch": ["x64", "arm64"] + }, + { + "target": "zip", + "arch": ["x64", "arm64"] + } + ], + "icon": "icons/icons/mac/icon.icns", + "artifactName": "${productName}-${arch}.${ext}", + "extendInfo": { + "NSAudioCaptureUsageDescription": "Recordly needs audio capture permission to record system audio.", + "NSCameraUsageDescription": "Recordly needs camera access to record webcam video.", + "NSMicrophoneUsageDescription": "Recordly needs microphone access to record voice audio.", + "NSCameraUseContinuityCameraDeviceType": true, + "com.apple.security.device.audio-input": true, + "CFBundleDocumentTypes": [ + { + "CFBundleTypeName": "Recordly Project", + "CFBundleTypeRole": "Editor", + "CFBundleTypeExtensions": ["recordly"], + "CFBundleTypeIconFile": "icon.icns", + "LSHandlerRank": "Owner", + "LSItemContentTypes": ["dev.recordly.app.project"] + } + ], + "UTExportedTypeDeclarations": [ + { + "UTTypeIdentifier": "dev.recordly.app.project", + "UTTypeDescription": "Recordly Project", + "UTTypeConformsTo": ["public.json"], + "UTTypeTagSpecification": { + "public.filename-extension": ["recordly"], + "public.mime-type": "application/x-recordly-project" + } + } + ] + } + }, + "linux": { + "target": [ + "AppImage" + ], + "icon": "icons/icons/png", + "artifactName": "${productName}-linux-x64.${ext}", + "category": "AudioVideo" + }, + "win": { + "target": [ + "nsis" + ], + "icon": "icons/icons/win/icon.ico" + , + "executableName": "Recordly", + "artifactName": "${productName}-windows-${arch}.${ext}" + } +} + diff --git a/electron/ipc/project/manager.test.ts b/electron/ipc/project/manager.test.ts index 3fa810da..50873092 100644 --- a/electron/ipc/project/manager.test.ts +++ b/electron/ipc/project/manager.test.ts @@ -53,7 +53,9 @@ describe("local media path policy", () => { await fs.mkdir(downloadsPath, { recursive: true }); await fs.writeFile(exportPath, "test-video"); - const { isAllowedLocalMediaPath, rememberApprovedLocalReadPath } = await import("./manager"); + const { isAllowedLocalMediaPath, rememberApprovedLocalReadPath } = await import( + "./manager" + ); await expect(isAllowedLocalMediaPath(exportPath)).resolves.toBe(false); @@ -71,7 +73,9 @@ describe("local media path policy", () => { it("allows approved media paths before the file exists", async () => { const pendingExportPath = path.join(tempRoot, "Downloads", "pending-export.mp4"); - const { isAllowedLocalMediaPath, rememberApprovedLocalReadPath } = await import("./manager"); + const { isAllowedLocalMediaPath, rememberApprovedLocalReadPath } = await import( + "./manager" + ); await rememberApprovedLocalReadPath(pendingExportPath); @@ -131,7 +135,9 @@ describe("local media path policy", () => { throw error; } - const { isAllowedLocalMediaPath, resolveApprovedLocalMediaPath } = await import("./manager"); + const { isAllowedLocalMediaPath, resolveApprovedLocalMediaPath } = await import( + "./manager" + ); await expect(isAllowedLocalMediaPath(symlinkInsideUserData)).resolves.toBe(false); await expect(resolveApprovedLocalMediaPath(symlinkInsideUserData)).resolves.toBeNull(); @@ -173,6 +179,29 @@ describe("local media path policy", () => { expect(result.project).toMatchObject({ videoPath }); }); + it("rejects invalid project payloads before approving media paths", async () => { + const downloadsPath = path.join(tempRoot, "Downloads"); + const videoPath = path.join(downloadsPath, "recording.mp4"); + const projectPath = path.join(tempPath, "invalid.recordly"); + await fs.mkdir(downloadsPath, { recursive: true }); + await fs.writeFile(videoPath, "test-video"); + await fs.writeFile( + projectPath, + JSON.stringify({ + videoPath, + editor: {}, + }), + "utf-8", + ); + + const { loadProjectFromPath, resolveApprovedLocalMediaPath } = await import("./manager"); + + const result = await loadProjectFromPath(projectPath); + expect(result.success).toBe(false); + expect(result.message).toBe("Invalid project file format"); + await expect(resolveApprovedLocalMediaPath(videoPath)).resolves.toBeNull(); + }); + it("approves editor audioRegions audioPath entries when loading a project", async () => { const downloadsPath = path.join(tempRoot, "Downloads"); const videoPath = path.join(tempPath, "recording.mp4"); @@ -187,9 +216,7 @@ describe("local media path policy", () => { version: 1, videoPath, editor: { - audioRegions: [ - { id: "a1", startMs: 0, endMs: 1000, audioPath, volume: 1 }, - ], + audioRegions: [{ id: "a1", startMs: 0, endMs: 1000, audioPath, volume: 1 }], }, }), "utf-8", diff --git a/electron/ipc/project/manager.ts b/electron/ipc/project/manager.ts index 2f6e4fc2..7828fa8b 100644 --- a/electron/ipc/project/manager.ts +++ b/electron/ipc/project/manager.ts @@ -403,6 +403,29 @@ export async function listProjectLibraryEntries() { }; } +function isLoadableProjectData(projectData: unknown) { + if (!projectData || typeof projectData !== "object" || Array.isArray(projectData)) { + return false; + } + + const candidate = projectData as { + version?: unknown; + projectId?: unknown; + videoPath?: unknown; + editor?: unknown; + }; + + return ( + typeof candidate.version === "number" && + (candidate.projectId === undefined || typeof candidate.projectId === "string") && + typeof candidate.videoPath === "string" && + candidate.videoPath.trim().length > 0 && + candidate.editor != null && + typeof candidate.editor === "object" && + !Array.isArray(candidate.editor) + ); +} + export async function loadProjectFromPath(projectPath: string) { const normalizedPath = normalizePath(projectPath); let project: unknown; @@ -416,6 +439,13 @@ export async function loadProjectFromPath(projectPath: string) { message: `Failed to read project file: ${error instanceof Error ? error.message : String(error)}`, }; } + if (!isLoadableProjectData(project)) { + return { + success: false, + canceled: false, + message: "Invalid project file format", + }; + } const mediaSources = await resolveProjectMediaSources(project); if (!mediaSources.success) { diff --git a/src/components/video-editor/projectDirtyState.test.ts b/src/components/video-editor/projectDirtyState.test.ts index 4cfb5257..68c5650f 100644 --- a/src/components/video-editor/projectDirtyState.test.ts +++ b/src/components/video-editor/projectDirtyState.test.ts @@ -64,4 +64,58 @@ describe("hasUnsavedProjectChanges", () => { expect(hasUnsavedProjectChanges(current, createProjectData())).toBe(true); }); + + it("ignores transient webcam media attachment changes", () => { + const saved = createProjectData({ + editor: { + ...createProjectData().editor, + webcam: { + enabled: false, + sourcePath: null, + timeOffsetMs: 0, + size: 28, + }, + }, + }); + const current = createProjectData({ + editor: { + ...createProjectData().editor, + webcam: { + enabled: true, + sourcePath: "/Users/test/webcam.mp4", + timeOffsetMs: 125, + size: 28, + }, + }, + }); + + expect(hasUnsavedProjectChanges(current, saved)).toBe(false); + }); + + it("detects persistent webcam presentation changes", () => { + const saved = createProjectData({ + editor: { + ...createProjectData().editor, + webcam: { + enabled: true, + sourcePath: "/Users/test/webcam.mp4", + timeOffsetMs: 0, + size: 28, + }, + }, + }); + const current = createProjectData({ + editor: { + ...createProjectData().editor, + webcam: { + enabled: true, + sourcePath: "/Users/test/webcam.mp4", + timeOffsetMs: 0, + size: 36, + }, + }, + }); + + expect(hasUnsavedProjectChanges(current, saved)).toBe(true); + }); });