fix(projects): tighten import and document project files

This commit is contained in:
webadderall committed 2026-06-15 13:43:08 +10:00
1 parent 262d961cfb
commit 6e7342d16c
4 files changed
+218 -86

No files matched your search

+101 -80
View File
@@ -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}"
}
}
+33 -6
View File
@@ -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",
+30
View File
@@ -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) {
@@ -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);
});
});