diff --git a/apps/web/src/app/projects/page.tsx b/apps/web/src/app/projects/page.tsx
index 3bb849b7..88b0a4e8 100644
--- a/apps/web/src/app/projects/page.tsx
+++ b/apps/web/src/app/projects/page.tsx
@@ -559,6 +559,7 @@ function ImportProjectButton() {
variant="outline"
className="flex px-5 md:px-6"
onClick={() => fileInputRef.current?.click()}
+ aria-label="Import project"
>
Import
diff --git a/apps/web/src/core/managers/project-manager.ts b/apps/web/src/core/managers/project-manager.ts
index 4ea00dc8..dfa29e2c 100644
--- a/apps/web/src/core/managers/project-manager.ts
+++ b/apps/web/src/core/managers/project-manager.ts
@@ -31,6 +31,8 @@ import { getElementFontFamilies } from "@/lib/timeline/element-utils";
import { getRaisedProjectFpsForImportedMedia } from "@/lib/fps/utils";
import type { MediaAsset } from "@/lib/media/types";
+const SUPPORTED_FORMAT_VERSION = 1;
+
export interface MigrationState {
isMigrating: boolean;
fromVersion: number | null;
@@ -651,7 +653,7 @@ export class ProjectManager {
}
const exportData = {
- formatVersion: 1,
+ formatVersion: SUPPORTED_FORMAT_VERSION,
exportedAt: new Date().toISOString(),
project: serializedProject,
};
@@ -701,13 +703,10 @@ export class ProjectManager {
return null;
}
- const data = parsed as Record;
-
if (
- !data ||
- typeof data !== "object" ||
- !("formatVersion" in data) ||
- !("project" in data)
+ !parsed ||
+ typeof parsed !== "object" ||
+ Array.isArray(parsed)
) {
toast.error("Invalid file format", {
description: "This does not appear to be an OpenCut project file",
@@ -715,12 +714,38 @@ export class ProjectManager {
return null;
}
- const serializedProject = data.project as SerializedProject;
+ const data = parsed as Record;
+
+ if (data.formatVersion !== SUPPORTED_FORMAT_VERSION) {
+ toast.error("Unsupported file version", {
+ description: `Expected formatVersion ${SUPPORTED_FORMAT_VERSION}, got ${JSON.stringify(data.formatVersion)}`,
+ });
+ return null;
+ }
if (
- !serializedProject?.metadata?.id ||
- !serializedProject?.metadata?.name ||
- !Array.isArray(serializedProject?.scenes)
+ !data.project ||
+ typeof data.project !== "object" ||
+ Array.isArray(data.project)
+ ) {
+ toast.error("Invalid project data", {
+ description: "The project data is incomplete or corrupted",
+ });
+ return null;
+ }
+
+ const serializedProject = data.project as SerializedProject;
+ const meta = serializedProject.metadata as unknown as
+ | Record
+ | undefined;
+
+ if (
+ !meta ||
+ typeof meta.id !== "string" ||
+ meta.id.trim() === "" ||
+ typeof meta.name !== "string" ||
+ meta.name.trim() === "" ||
+ !Array.isArray(serializedProject.scenes)
) {
toast.error("Invalid project data", {
description: "The project data is incomplete or corrupted",
diff --git a/apps/web/src/services/storage/service.ts b/apps/web/src/services/storage/service.ts
index e29be15b..23983c31 100644
--- a/apps/web/src/services/storage/service.ts
+++ b/apps/web/src/services/storage/service.ts
@@ -532,6 +532,9 @@ class StorageService {
}: {
serializedProject: SerializedProject;
}): Promise {
+ // Migrations must complete before any write — otherwise the import races
+ // with the memoized ensureMigrations() and may end up in a half-migrated store.
+ await this.ensureMigrations();
await this.projectsAdapter.set(
serializedProject.metadata.id,
serializedProject,