>;
timeline: TimelineState;
@@ -44,6 +46,8 @@ export type VideoPlayerProps = {
export function TimelineComposition({
timelineData,
isRendering,
+ compositionWidth,
+ compositionHeight,
selectedItem,
setSelectedItem,
timeline,
@@ -52,8 +56,13 @@ export function TimelineComposition({
}: TimelineCompositionProps) {
// Resolve pixels per second based on rendering mode
const resolvedPixelsPerSecond = isRendering ? (getPixelsPerSecond as number) : (getPixelsPerSecond as () => number)();
+
+ if (!timelineData?.length) {
+ return ;
+ }
+
// Get all transitions from timelineData
- const allTransitions = timelineData[0].transitions;
+ const allTransitions = timelineData[0].transitions ?? {};
// Step 1: Group scrubbers by trackIndex
const trackGroups: {
@@ -120,6 +129,7 @@ export function TimelineComposition({
const imageUrl = isRendering
? scrubber.mediaUrlRemote || scrubber.mediaUrlLocal
: scrubber.mediaUrlLocal || scrubber.mediaUrlRemote;
+ if (!imageUrl) break;
content = (
-
+
);
break;
@@ -137,6 +147,7 @@ export function TimelineComposition({
const videoUrl = isRendering
? scrubber.mediaUrlRemote || scrubber.mediaUrlLocal
: scrubber.mediaUrlLocal || scrubber.mediaUrlRemote;
+ if (!videoUrl) break;
content = (
);
@@ -158,36 +171,47 @@ export function TimelineComposition({
const audioUrl = isRendering
? scrubber.mediaUrlRemote || scrubber.mediaUrlLocal
: scrubber.mediaUrlLocal || scrubber.mediaUrlRemote;
+ if (!audioUrl) break;
content = (
);
break;
}
default:
- console.warn(`Unknown media type: ${scrubber.mediaType}`);
break;
}
return content;
};
- // Helper function to get transition presentation
- const getTransitionPresentation = (transition: Transition) => {
+ // Helper function to get transition presentation.
+ // Each per-effect helper returns its own TransitionPresentation generic; the union of those
+ // is not assignable to any single TransitionPresentation
, so we erase the prop generic to a
+ // permissive `Record` (Remotion accepts any presentation factory at runtime).
+ type AnyPresentation = TransitionPresentation>;
+ const safeTransitionWidth = compositionWidth && compositionWidth > 0 ? compositionWidth : 1920;
+ const safeTransitionHeight = compositionHeight && compositionHeight > 0 ? compositionHeight : 1080;
+ const getTransitionPresentation = (transition: Transition): AnyPresentation => {
+ const cast = (p: unknown) => p as AnyPresentation;
switch (transition.presentation) {
case "fade":
- return fade();
+ return cast(fade());
case "wipe":
- return wipe();
+ return cast(wipe());
case "slide":
- return slide();
+ return cast(slide());
case "flip":
- return flip();
+ return cast(flip());
case "iris":
- return iris({ width: 1000, height: 1000 });
+ return cast(iris({ width: safeTransitionWidth, height: safeTransitionHeight }));
+ default:
+ return cast(fade());
}
};
@@ -243,7 +267,6 @@ export function TimelineComposition({
transitionSeriesElements.push(
,
@@ -265,7 +288,6 @@ export function TimelineComposition({
transitionSeriesElements.push(
,
@@ -321,7 +343,6 @@ export function TimelineComposition({
transitionSeriesElements.push(
,
@@ -378,7 +399,6 @@ export function TimelineComposition({
transitionSeriesElements.push(
,
@@ -486,6 +506,8 @@ export function VideoPlayer({
timelineData,
durationInFrames,
isRendering: false,
+ compositionWidth: safeWidth,
+ compositionHeight: safeHeight,
selectedItem,
setSelectedItem,
timeline,
diff --git a/app/videorender/Composition.tsx b/app/videorender/Composition.tsx
index 6cb2a68d..0ead9622 100644
--- a/app/videorender/Composition.tsx
+++ b/app/videorender/Composition.tsx
@@ -3,7 +3,6 @@ import { TimelineComposition } from "../video-compositions/VideoPlayer";
export default function RenderComposition() {
const inputProps = getInputProps();
- console.log("Input props:", inputProps);
return (
config,
});
-console.log(bundleLocation);
-
-// Ensure output directory exists
+// Ensure output directory exists for temporary render files
if (!fs.existsSync("out")) {
fs.mkdirSync("out", { recursive: true });
}
-const app = express();
-app.use(express.json());
-app.use(cors());
+// ─── BullMQ render queue ──────────────────────────────────────────────────────
-// Static file serving for the out/ directory
-app.use(
- "/media",
- express.static(path.resolve("out"), {
- dotfiles: "deny",
- index: false,
- }),
-);
+function createRedisConnection() {
+ return new IORedis(process.env.REDIS_URL ?? "redis://localhost:6379", {
+ maxRetriesPerRequest: null,
+ enableReadyCheck: false,
+ });
+}
-// Configure multer for file uploads
-const storage = multer.diskStorage({
- destination: (req, file, cb) => {
- // Ensure out directory exists
- if (!fs.existsSync("out")) {
- fs.mkdirSync("out", { recursive: true });
- }
- cb(null, "out/");
- },
- filename: (req, file, cb) => {
- // Generate unique filename with timestamp
- const timestamp = Date.now();
- const originalName = file.originalname;
- const extension = path.extname(originalName);
- const nameWithoutExt = path.basename(originalName, extension);
- const uniqueName = `${nameWithoutExt}_${timestamp}${extension}`;
- cb(null, uniqueName);
- },
-});
+interface RenderJobData {
+ userId: string;
+ projectId: string;
+ renderJobId: string;
+ contentFingerprint: string;
+ codec: "h264" | "h265" | "vp9";
+ crf: number;
+ resolutionPreset: ExportResolutionPreset;
+ outputFileName: string;
+ advancedMode: boolean;
+ jpegQuality?: number;
+ x264Preset?: X264Preset;
+ muted?: boolean;
+ inputProps: {
+ timelineData: unknown;
+ durationInFrames: number;
+ compositionWidth: number;
+ compositionHeight: number;
+ getPixelsPerSecond: number;
+ isRendering: boolean;
+ };
+}
-const upload = multer({
- storage,
- limits: {
- fileSize: 500 * 1024 * 1024, // 500MB limit (we'll do no limits later)
- },
- fileFilter: (req, file, cb) => {
- // Accept common media file types
- const allowedTypes = /\.(mp4|webm|mov|avi|mkv|flv|wmv|m4v|mp3|wav|aac|ogg|flac|jpg|jpeg|png|gif|bmp|webp)$/i;
- if (allowedTypes.test(file.originalname)) {
- cb(null, true);
+function extForCodec(codec: string) {
+ return codec === "vp9" ? "webm" : "mp4";
+}
+function mimeForCodec(codec: string) {
+ return codec === "vp9" ? "video/webm" : "video/mp4";
+}
+
+interface CachedRenderRow {
+ id: string;
+ file_name: string;
+ r2_video_key: string;
+ r2_thumb_key: string | null;
+ codec: string;
+}
+
+async function ensureProjectRendersTable(): Promise {
+ await db.query(`
+ CREATE TABLE IF NOT EXISTS project_renders (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
+ user_id TEXT NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
+ render_job_id UUID NOT NULL,
+ content_fingerprint TEXT NOT NULL,
+ file_name TEXT NOT NULL,
+ codec TEXT NOT NULL,
+ width INT NOT NULL,
+ height INT NOT NULL,
+ duration_frames INT,
+ crf INT,
+ resolution_preset TEXT,
+ r2_video_key TEXT NOT NULL,
+ r2_thumb_key TEXT,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
+ );
+ CREATE INDEX IF NOT EXISTS idx_project_renders_project_created
+ ON project_renders(project_id, created_at DESC);
+ CREATE INDEX IF NOT EXISTS idx_project_renders_fingerprint
+ ON project_renders(project_id, user_id, content_fingerprint, created_at DESC);
+ `);
+}
+
+async function assertProjectOwned(userId: string, projectId: string): Promise {
+ const { rows } = await db.query(
+ `SELECT id FROM projects WHERE id = $1::uuid AND user_id = $2`,
+ [projectId, userId],
+ );
+ return rows.length > 0;
+}
+
+async function findCachedRender(
+ projectId: string,
+ userId: string,
+ fingerprint: string,
+): Promise {
+ const { rows } = await db.query(
+ `SELECT id, file_name, r2_video_key, r2_thumb_key, codec
+ FROM project_renders
+ WHERE project_id = $1::uuid AND user_id = $2 AND content_fingerprint = $3
+ ORDER BY created_at DESC
+ LIMIT 1`,
+ [projectId, userId, fingerprint],
+ );
+ return (rows[0] as CachedRenderRow | undefined) ?? null;
+}
+
+async function signRenderAssetUrls(
+ row: Pick,
+): Promise<{ downloadUrl: string; previewUrl: string; thumbnailUrl: string | null }> {
+ const safeName = row.file_name.replace(/"/g, "");
+ const downloadUrl = await getSignedUrl(
+ r2,
+ new GetObjectCommand({
+ Bucket: RENDERS_BUCKET,
+ Key: row.r2_video_key,
+ ResponseContentDisposition: `attachment; filename="${safeName}"`,
+ }),
+ { expiresIn: 3600 },
+ );
+ const previewUrl = await getSignedUrl(
+ r2,
+ new GetObjectCommand({
+ Bucket: RENDERS_BUCKET,
+ Key: row.r2_video_key,
+ ResponseContentDisposition: `inline; filename="${safeName}"`,
+ }),
+ { expiresIn: 3600 },
+ );
+ let thumbnailUrl: string | null = null;
+ if (row.r2_thumb_key) {
+ thumbnailUrl = await getSignedUrl(
+ r2,
+ new GetObjectCommand({ Bucket: RENDERS_BUCKET, Key: row.r2_thumb_key }),
+ { expiresIn: 3600 },
+ );
+ }
+ return { downloadUrl, previewUrl, thumbnailUrl };
+}
+
+function isUserOwnedRenderKey(key: string, userId: string): boolean {
+ const prefix = `${userId}/`;
+ return key.startsWith(prefix) && !key.includes("..") && key.length > prefix.length;
+}
+
+async function deleteRenderR2Objects(
+ userId: string,
+ videoKey: string,
+ thumbKey: string | null,
+): Promise {
+ if (!RENDERS_BUCKET) {
+ throw new Error("Renders bucket not configured");
+ }
+ if (!isUserOwnedRenderKey(videoKey, userId)) {
+ throw new Error("Invalid render video key");
+ }
+ await r2.send(new DeleteObjectCommand({ Bucket: RENDERS_BUCKET, Key: videoKey }));
+ if (thumbKey) {
+ if (!isUserOwnedRenderKey(thumbKey, userId)) {
+ console.warn(`Skipping invalid thumb key on render delete: ${thumbKey}`);
} else {
- cb(new Error("Invalid file type. Only media files are allowed."));
+ try {
+ await r2.send(new DeleteObjectCommand({ Bucket: RENDERS_BUCKET, Key: thumbKey }));
+ } catch (err) {
+ console.warn(`Failed to delete render thumbnail ${thumbKey}:`, err);
+ }
}
- },
-});
+ }
+}
-// List files in out/ directory
-app.get("/media", (req: Request, res: Response): void => {
+async function extractThumbnail(videoPath: string, thumbPath: string): Promise {
try {
- const outDir = path.resolve("out");
- if (!fs.existsSync(outDir)) {
- res.json({ files: [] });
- return;
+ await execFileAsync(
+ "npx",
+ [
+ "remotion",
+ "ffmpeg",
+ "-y",
+ "-ss",
+ "0",
+ "-i",
+ videoPath,
+ "-frames:v",
+ "1",
+ "-vf",
+ "scale=320:-2",
+ "-q:v",
+ "5",
+ thumbPath,
+ ],
+ { timeout: 120_000, cwd: process.cwd() },
+ );
+ return fs.existsSync(thumbPath);
+ } catch (err) {
+ console.warn("Thumbnail extraction failed:", err);
+ return false;
+ }
+}
+
+const renderQueue = new Queue("renders", { connection: createRedisConnection() });
+const renderQueueEvents = new QueueEvents("renders", { connection: createRedisConnection() });
+
+const renderWorker = new Worker<
+ RenderJobData,
+ { downloadUrl: string; fileName: string; renderId: string }
+>(
+ "renders",
+ async (job) => {
+ const {
+ userId,
+ projectId,
+ renderJobId,
+ contentFingerprint,
+ inputProps: rawInputProps,
+ codec = "h264",
+ crf = 28,
+ resolutionPreset = "1080p",
+ outputFileName,
+ advancedMode = false,
+ jpegQuality: jpegQualityOverride,
+ x264Preset: x264PresetOverride,
+ muted = false,
+ } = job.data;
+ const ext = extForCodec(codec);
+ const downloadFileName = sanitizeExportFileName(outputFileName, ext);
+ const localOutputPath = `out/${renderJobId}.${ext}`;
+ const effectiveCrf = clampExportCrf(crf, advancedMode);
+
+ const capped = capExportDimensions(
+ rawInputProps.compositionWidth,
+ rawInputProps.compositionHeight,
+ resolutionPreset,
+ );
+ if (capped.scaled) {
+ console.log(
+ `📐 Export resolution capped to ${capped.width}×${capped.height} (preset: ${resolutionPreset})`,
+ );
+ }
+ const inputProps = {
+ ...rawInputProps,
+ compositionWidth: capped.width,
+ compositionHeight: capped.height,
+ };
+ const tuning = getRemotionRenderTuning(capped.width, capped.height);
+ if (
+ typeof jpegQualityOverride === "number" &&
+ jpegQualityOverride >= 60 &&
+ jpegQualityOverride <= 100
+ ) {
+ tuning.jpegQuality = Math.round(jpegQualityOverride);
+ }
+ if (
+ codec === "h264" &&
+ x264PresetOverride &&
+ (X264_PRESETS as readonly string[]).includes(x264PresetOverride)
+ ) {
+ tuning.x264Preset = x264PresetOverride;
}
- const files = fs
- .readdirSync(outDir)
- .map((filename) => {
- const filePath = path.join(outDir, filename);
- const stats = fs.statSync(filePath);
- return {
- name: filename,
- url: `/media/${encodeURIComponent(filename)}`,
- size: stats.size,
- modified: stats.mtime,
- isDirectory: stats.isDirectory(),
- };
- })
- .filter((file) => !file.isDirectory); // Only show files, not directories
+ // Headless Chrome launched by Remotion loads from Remotion's own bundle
+ // server (e.g. port 3001), not from this Express app. Relative asset URLs
+ // like /renderer/assets/{id}/file would resolve against that bundle server
+ // and 404. Rewrite them to absolute loopback URLs so Chrome fetches from
+ // this Express app regardless of environment (local dev or Docker).
+ const rendererPort = process.env.PORT ?? "8000";
+ const absInputProps = {
+ ...inputProps,
+ timelineData: JSON.parse(
+ JSON.stringify(inputProps.timelineData).replace(
+ /\/renderer\/assets\//g,
+ `http://127.0.0.1:${rendererPort}/renderer/assets/`,
+ ),
+ ) as unknown,
+ };
+
+ await job.updateProgress(5);
+
+ const composition = await selectComposition({
+ serveUrl: bundleLocation,
+ id: compositionId,
+ inputProps: absInputProps,
+ });
+
+ await job.updateProgress(10);
+
+ let lastReportedProgress = 10;
+ const useCrf = effectiveCrf > 0 && effectiveCrf <= 51 ? effectiveCrf : undefined;
+
+ await renderMedia({
+ composition,
+ serveUrl: bundleLocation,
+ codec,
+ outputLocation: localOutputPath,
+ inputProps: absInputProps,
+ concurrency: tuning.concurrency,
+ disallowParallelEncoding: tuning.disallowParallelEncoding,
+ offthreadVideoCacheSizeInBytes: tuning.offthreadVideoCacheSizeInBytes,
+ jpegQuality: tuning.jpegQuality,
+ imageFormat: "jpeg",
+ crf: useCrf,
+ x264Preset: codec === "h264" ? tuning.x264Preset : undefined,
+ colorSpace: "bt709",
+ muted,
+ logLevel: "info",
+ onProgress: ({ progress }) => {
+ const percent = Math.round(10 + progress * 80);
+ if (percent >= lastReportedProgress + 2) {
+ lastReportedProgress = percent;
+ void job.updateProgress(percent);
+ }
+ },
+ ffmpegOverride:
+ codec === "h265"
+ ? ({ args }) => [...args, "-tag:v", "hvc1"]
+ : undefined,
+ timeoutInMilliseconds: 900000,
+ });
- res.json({ files });
- } catch (error) {
- console.error("Error listing files:", error);
- res.status(500).json({ error: "Failed to list files" });
+ console.log("✅ Render completed — uploading to R2");
+ await job.updateProgress(92);
+
+ const renderKey = `${userId}/${renderJobId}.${ext}`;
+ const fileStream = fs.createReadStream(localOutputPath);
+ const upload = new Upload({
+ client: r2,
+ params: {
+ Bucket: RENDERS_BUCKET,
+ Key: renderKey,
+ Body: fileStream,
+ ContentType: mimeForCodec(codec),
+ },
+ queueSize: 4,
+ partSize: 10 * 1024 * 1024,
+ });
+ await upload.done();
+
+ await job.updateProgress(94);
+
+ const thumbPath = `out/${renderJobId}-thumb.jpg`;
+ let r2ThumbKey: string | null = null;
+ if (await extractThumbnail(localOutputPath, thumbPath)) {
+ r2ThumbKey = `${userId}/thumbs/${renderJobId}.jpg`;
+ const thumbStream = fs.createReadStream(thumbPath);
+ const thumbUpload = new Upload({
+ client: r2,
+ params: {
+ Bucket: RENDERS_BUCKET,
+ Key: r2ThumbKey,
+ Body: thumbStream,
+ ContentType: "image/jpeg",
+ },
+ });
+ await thumbUpload.done();
+ try {
+ fs.unlinkSync(thumbPath);
+ } catch {
+ /* ignore */
+ }
+ }
+
+ await job.updateProgress(97);
+
+ const { rows: insertRows } = await db.query(
+ `INSERT INTO project_renders (
+ project_id, user_id, render_job_id, content_fingerprint, file_name, codec,
+ width, height, duration_frames, crf, resolution_preset, r2_video_key, r2_thumb_key
+ ) VALUES ($1::uuid, $2, $3::uuid, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
+ RETURNING id`,
+ [
+ projectId,
+ userId,
+ renderJobId,
+ contentFingerprint,
+ downloadFileName,
+ codec,
+ capped.width,
+ capped.height,
+ rawInputProps.durationInFrames,
+ effectiveCrf,
+ resolutionPreset,
+ renderKey,
+ r2ThumbKey,
+ ],
+ );
+ const renderId = String(insertRows[0].id);
+
+ const { downloadUrl } = await signRenderAssetUrls({
+ file_name: downloadFileName,
+ r2_video_key: renderKey,
+ r2_thumb_key: r2ThumbKey,
+ codec,
+ });
+
+ try {
+ fs.unlinkSync(localOutputPath);
+ } catch {
+ /* ignore */
+ }
+
+ console.log(`📦 Render uploaded: ${renderKey}`);
+ return { downloadUrl, fileName: downloadFileName, renderId };
+ },
+ {
+ connection: createRedisConnection(),
+ concurrency: 1,
+ },
+);
+
+renderWorker.on("failed", (job, err) => {
+ console.error(`❌ Render job ${job?.id} failed:`, err.message);
+ const renderJobId = job?.data?.renderJobId;
+ if (renderJobId) {
+ try {
+ const p = `out/${renderJobId}.mp4`;
+ if (fs.existsSync(p)) fs.unlinkSync(p);
+ } catch {}
}
});
-// File upload endpoint
-app.post("/upload", upload.single("media"), (req: Request, res: Response): void => {
+async function getAuthenticatedUserId(req: Request): Promise {
+ const session = await auth.api.getSession({ headers: req.headers });
+ return session?.user?.id ?? null;
+}
+
+// ─── Express app ──────────────────────────────────────────────────────────────
+
+const app = express();
+app.use(express.json());
+app.use(cors());
+
+// ─── UUID helper ──────────────────────────────────────────────────────────────
+
+function generateUUID(): string {
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
+ const r = (Math.random() * 16) | 0;
+ return (c === "x" ? r : (r & 0x3) | 0x8).toString(16);
+ });
+}
+
+// ─── Renderer-internal asset proxy (no auth — called by headless Chrome) ─────
+// Headless Chrome launched by Remotion has no session cookies. This route lets
+// it fetch assets during rendering. The path matches `mediaUrlLocal` values
+// stored as `/renderer/assets/{id}/file` in the timeline JSON.
+
+app.get("/renderer/assets/:assetId/file", async (req: Request, res: Response): Promise => {
+ const { assetId } = req.params;
+ if (!UUID_PATTERN.test(assetId)) {
+ res.status(400).end();
+ return;
+ }
try {
- if (!req.file) {
- res.status(400).json({ error: "No file uploaded" });
+ const { rows } = await db.query<{ r2_key: string; mime_type: string }>(
+ `SELECT r2_key, mime_type FROM assets WHERE id = $1 AND deleted_at IS NULL AND status = 'ready' LIMIT 1`,
+ [assetId],
+ );
+ if (rows.length === 0) {
+ res.status(404).end();
return;
}
+ const object = await r2.send(new GetObjectCommand({ Bucket: ASSETS_BUCKET, Key: rows[0].r2_key }));
+ const body = object.Body as NodeJS.ReadableStream | undefined;
+ if (!body || typeof (body as { pipe?: unknown }).pipe !== "function") {
+ res.status(500).end();
+ return;
+ }
+ res.setHeader("Content-Type", object.ContentType || rows[0].mime_type || "application/octet-stream");
+ res.setHeader("Cache-Control", "private, max-age=300");
+ if (typeof object.ContentLength === "number") res.setHeader("Content-Length", String(object.ContentLength));
+ body.on("error", () => { if (!res.headersSent) res.status(500).end(); else res.end(); });
+ body.pipe(res);
+ } catch (err) {
+ console.error("renderer asset proxy error:", err);
+ res.status(500).end();
+ }
+});
+
+// ─── Health check ─────────────────────────────────────────────────────────────
+
+app.get("/health", (_req: Request, res: Response) => {
+ const used = process.memoryUsage();
+ res.json({
+ status: "ok",
+ memory: {
+ rss: `${Math.round(used.rss / 1024 / 1024)} MB`,
+ heapTotal: `${Math.round(used.heapTotal / 1024 / 1024)} MB`,
+ heapUsed: `${Math.round(used.heapUsed / 1024 / 1024)} MB`,
+ },
+ uptime: `${Math.round(process.uptime())} seconds`,
+ });
+});
- const fileUrl = `/media/${encodeURIComponent(req.file.filename)}`;
- const fullUrl = `http://localhost:${port}${fileUrl}`; // Direct backend URL for Remotion
+app.get("/assets", async (req: Request, res: Response): Promise => {
+ const userId = await getAuthenticatedUserId(req);
+ if (!userId) {
+ res.status(401).json({ error: "Unauthorized" });
+ return;
+ }
- console.log(`📁 File uploaded: ${req.file.originalname} -> ${req.file.filename}`);
+ const projectId = typeof req.query.projectId === "string" ? req.query.projectId : null;
+ if (projectId && !UUID_PATTERN.test(projectId)) {
+ res.status(400).json({ error: "Invalid projectId" });
+ return;
+ }
+
+ try {
+ const { rows } = await db.query<{
+ id: string;
+ filename: string;
+ media_type: string | null;
+ mime_type: string;
+ file_size: string | null;
+ width: number | null;
+ height: number | null;
+ duration_seconds: number | null;
+ created_at: Date | string;
+ }>(
+ `SELECT id, filename, media_type, mime_type, file_size, width, height, duration_seconds, created_at
+ FROM assets
+ WHERE user_id = $1
+ AND deleted_at IS NULL
+ AND status = 'ready'
+ AND ($2::uuid IS NULL OR project_id = $2::uuid)
+ ORDER BY created_at DESC`,
+ [userId, projectId],
+ );
res.json({
- success: true,
- filename: req.file.filename,
- originalName: req.file.originalname,
- url: fileUrl,
- fullUrl: fullUrl,
- size: req.file.size,
- path: req.file.path,
+ assets: rows.map((row) => ({
+ id: row.id,
+ filename: row.filename,
+ mediaType: row.media_type,
+ mimeType: row.mime_type,
+ fileSize: row.file_size ? Number(row.file_size) : null,
+ width: row.width,
+ height: row.height,
+ durationInSeconds: row.duration_seconds,
+ createdAt: row.created_at instanceof Date ? row.created_at.toISOString() : String(row.created_at),
+ assetUrl: getAssetUrl(row.id),
+ })),
});
- } catch (error) {
- console.error("Upload error:", error);
- res.status(500).json({ error: "File upload failed" });
+ } catch (err) {
+ console.error("list assets error:", err);
+ res.status(500).json({ error: "Failed to list assets" });
}
});
-// Bulk file upload endpoint
-app.post("/upload-multiple", upload.array("media", 10), (req: Request, res: Response): void => {
+app.get("/assets/:assetId/file", async (req: Request, res: Response): Promise => {
+ const userId = await getAuthenticatedUserId(req);
+ if (!userId) {
+ res.status(401).json({ error: "Unauthorized" });
+ return;
+ }
+
+ const { assetId } = req.params;
try {
- if (!req.files || req.files.length === 0) {
- res.status(400).json({ error: "No files uploaded" });
+ const { rows } = await db.query<{ r2_key: string; mime_type: string }>(
+ `SELECT r2_key, mime_type
+ FROM assets
+ WHERE id = $1
+ AND user_id = $2
+ AND deleted_at IS NULL
+ AND status = 'ready'
+ LIMIT 1`,
+ [assetId, userId],
+ );
+
+ if (rows.length === 0) {
+ res.status(404).json({ error: "Asset not found" });
return;
}
- const uploadedFiles = (req.files as Express.Multer.File[]).map((file) => ({
- filename: file.filename,
- originalName: file.originalname,
- url: `/media/${encodeURIComponent(file.filename)}`,
- fullUrl: `http://localhost:${port}/media/${encodeURIComponent(file.filename)}`, // Direct backend URL for Remotion
- size: file.size,
- path: file.path,
- }));
+ const r2Key = rows[0].r2_key;
+ const mimeType = rows[0].mime_type || "application/octet-stream";
+ const object = await r2.send(
+ new GetObjectCommand({
+ Bucket: ASSETS_BUCKET,
+ Key: r2Key,
+ }),
+ );
- console.log(`📁 ${uploadedFiles.length} files uploaded`);
+ const body = object.Body as NodeJS.ReadableStream | undefined;
+ if (!body || typeof (body as { pipe?: unknown }).pipe !== "function") {
+ res.status(500).json({ error: "Invalid asset stream" });
+ return;
+ }
- res.json({
- success: true,
- files: uploadedFiles,
+ res.setHeader("Content-Type", object.ContentType || mimeType);
+ res.setHeader("Cache-Control", "private, max-age=60");
+ if (typeof object.ContentLength === "number") {
+ res.setHeader("Content-Length", String(object.ContentLength));
+ }
+
+ body.on("error", (streamErr) => {
+ console.error("asset stream error:", streamErr);
+ if (!res.headersSent) {
+ res.status(500).json({ error: "Asset stream failed" });
+ } else {
+ res.end();
+ }
});
- } catch (error) {
- console.error("Bulk upload error:", error);
- res.status(500).json({ error: "Bulk file upload failed" });
+
+ body.pipe(res);
+ } catch (err) {
+ console.error("get asset file error:", err);
+ res.status(500).json({ error: "Failed to fetch asset file" });
}
});
-// Clone/copy media file endpoint
-app.post("/clone-media", (req: Request, res: Response): void => {
+// ─── POST /assets/initiate-upload ─────────────────────────────────────────────
+// Dedup-aware upload initiation.
+//
+// If the content hash already exists and the R2 object is ready, we skip the
+// upload entirely and create the per-user asset record pointing to the shared
+// object (returns { alreadyExists: true, assetUrl }).
+//
+// If the hash is new (or a concurrent upload is in-progress), we upsert the
+// r2_objects row, create the asset record, and then upload file bytes to this
+// renderer service at /assets/upload/:assetId (same-origin, no browser→R2 CORS).
+
+app.post("/assets/initiate-upload", async (req: Request, res: Response): Promise => {
+ const userId = await getAuthenticatedUserId(req);
+ if (!userId) {
+ res.status(401).json({ error: "Unauthorized" });
+ return;
+ }
+
+ const { assetId, filename, fileSize, mimeType, mediaType, contentHash, projectId } = req.body as {
+ assetId: string;
+ filename: string;
+ fileSize: number;
+ mimeType: string;
+ mediaType: string;
+ contentHash: string; // SHA-256 hex from browser
+ projectId?: string | null;
+ };
+
+ if (!assetId || !filename || !mimeType || !contentHash) {
+ res.status(400).json({ error: "assetId, filename, mimeType, and contentHash are required" });
+ return;
+ }
+
+ const ext = path.extname(filename).toLowerCase();
+ // Content-addressed key — shared across all users with the same file
+ const r2Key = `objects/${contentHash}${ext}`;
try {
- const { filename, originalName, suffix } = req.body;
+ // ── Dedup check ──────────────────────────────────────────────────────────
+ // Upsert into r2_objects. If a row already exists with this hash, the
+ // no-op UPDATE still triggers RETURNING so we get back the current status.
+ const { rows: objRows } = await db.query<{ status: string }>(
+ `INSERT INTO r2_objects (content_hash, r2_key, file_size, mime_type, status)
+ VALUES ($1, $2, $3, $4, 'pending')
+ ON CONFLICT (content_hash)
+ DO UPDATE SET content_hash = EXCLUDED.content_hash
+ RETURNING status`,
+ [contentHash, r2Key, fileSize || 0, mimeType],
+ );
+
+ const objectStatus = objRows[0]?.status ?? "pending";
+
+ if (objectStatus === "ready") {
+ // ── Fast-path: file already in R2 — create per-user asset record only ──
+ await db.query(
+ `INSERT INTO assets
+ (id, user_id, project_id, content_hash, r2_key, filename, file_size,
+ mime_type, media_type, status)
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,'ready')
+ ON CONFLICT (id) DO NOTHING`,
+ [
+ assetId,
+ userId,
+ projectId ?? null,
+ contentHash,
+ r2Key,
+ filename,
+ fileSize || null,
+ mimeType,
+ mediaType || null,
+ ],
+ );
- if (!filename) {
- res.status(400).json({ error: "Filename is required" });
+ console.log(`⚡ Dedup hit: ${filename} (${contentHash.slice(0, 8)}…)`);
+ res.json({ alreadyExists: true, assetId, assetUrl: getAssetUrl(assetId) });
return;
}
- const decodedFilename = decodeURIComponent(filename);
- const sourcePath = path.resolve("out", decodedFilename);
+ // ── Normal path: create asset record in 'uploading' state ───────────────
+ await db.query(
+ `INSERT INTO assets
+ (id, user_id, project_id, content_hash, r2_key, filename, file_size,
+ mime_type, media_type, status)
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,'uploading')
+ ON CONFLICT (id) DO UPDATE
+ SET content_hash=$4, r2_key=$5, filename=$6, file_size=$7,
+ mime_type=$8, media_type=$9, status='uploading'`,
+ [assetId, userId, projectId ?? null, contentHash, r2Key, filename, fileSize || null, mimeType, mediaType || null],
+ );
- // Security check - ensure source file is in the out directory
- if (!sourcePath.startsWith(path.resolve("out"))) {
- res.status(403).json({ error: "Access denied" });
+ console.log(`📤 Upload initiated: ${filename} → ${r2Key}`);
+ res.json({ assetId, r2Key, assetUrl: getAssetUrl(assetId) });
+ } catch (err) {
+ console.error("initiate-upload error:", err);
+ res.status(500).json({ error: "Failed to initiate upload" });
+ }
+});
+
+// ─── PUT /assets/upload/:assetId ──────────────────────────────────────────────
+// Receives raw bytes from browser and uploads to R2 server-side.
+// This keeps browser traffic same-origin and avoids R2 CORS preflight failures.
+
+app.put(
+ "/assets/upload/:assetId",
+ async (req: Request, res: Response): Promise => {
+ const userId = await getAuthenticatedUserId(req);
+ if (!userId) {
+ res.status(401).json({ error: "Unauthorized" });
return;
}
- if (!fs.existsSync(sourcePath)) {
- res.status(404).json({ error: "Source file not found" });
+ const { assetId } = req.params;
+ if (!assetId) {
+ res.status(400).json({ error: "assetId is required" });
+ return;
+ }
+ const declaredLength = Number(req.headers["content-length"] || 0);
+ if (declaredLength > MAX_ASSET_UPLOAD_BYTES) {
+ res.status(413).json({ error: "File too large" });
return;
}
- // Generate new filename with timestamp and suffix
- const timestamp = Date.now();
- const sourceExtension = path.extname(decodedFilename);
- const sourceNameWithoutExt = path.basename(decodedFilename, sourceExtension);
- const newFilename = `${sourceNameWithoutExt}_${suffix}_${timestamp}${sourceExtension}`;
- const destPath = path.resolve("out", newFilename);
+ try {
+ const { rows } = await db.query<{ r2_key: string; mime_type: string; content_hash: string | null }>(
+ `SELECT r2_key, mime_type, content_hash
+ FROM assets
+ WHERE id=$1 AND user_id=$2 AND deleted_at IS NULL`,
+ [assetId, userId],
+ );
- // Copy the file
- fs.copyFileSync(sourcePath, destPath);
+ if (rows.length === 0) {
+ res.status(404).json({ error: "Asset not found" });
+ return;
+ }
+
+ const r2Key = rows[0].r2_key;
+ const fallbackMimeType = rows[0].mime_type || "application/octet-stream";
+ const reqMimeType = req.headers["content-type"];
+ const contentType = typeof reqMimeType === "string" ? reqMimeType : fallbackMimeType;
+ let actualFileSize = 0;
+ const sizeGuardStream = new Transform({
+ transform(chunk, _encoding, callback) {
+ const chunkSize = Buffer.isBuffer(chunk) ? chunk.length : Buffer.byteLength(String(chunk));
+ actualFileSize += chunkSize;
+ if (actualFileSize > MAX_ASSET_UPLOAD_BYTES) {
+ callback(new Error("UPLOAD_TOO_LARGE"));
+ return;
+ }
+ callback(null, chunk);
+ },
+ });
+
+ req.on("aborted", () => {
+ sizeGuardStream.destroy(new Error("UPLOAD_ABORTED"));
+ });
+ req.on("error", (streamErr) => {
+ sizeGuardStream.destroy(streamErr);
+ });
+ req.pipe(sizeGuardStream);
- const fileStats = fs.statSync(destPath);
- const fileUrl = `/media/${encodeURIComponent(newFilename)}`;
- const fullUrl = `http://localhost:${port}${fileUrl}`;
+ const upload = new Upload({
+ client: r2,
+ params: {
+ Bucket: ASSETS_BUCKET,
+ Key: r2Key,
+ Body: sizeGuardStream,
+ ContentType: contentType,
+ },
+ queueSize: 4,
+ partSize: 10 * 1024 * 1024,
+ });
+ await upload.done();
- console.log(`📋 File cloned: ${decodedFilename} -> ${newFilename}`);
+ if (actualFileSize === 0) {
+ await r2.send(
+ new DeleteObjectCommand({
+ Bucket: ASSETS_BUCKET,
+ Key: r2Key,
+ }),
+ );
+ res.status(400).json({ error: "File body is required" });
+ return;
+ }
+ await db.query(
+ `UPDATE assets
+ SET file_size = $3
+ WHERE id = $1 AND user_id = $2`,
+ [assetId, userId, actualFileSize],
+ );
+ if (rows[0].content_hash) {
+ await db.query(
+ `UPDATE r2_objects
+ SET file_size = $2
+ WHERE content_hash = $1`,
+ [rows[0].content_hash, actualFileSize],
+ );
+ }
+
+ res.json({ success: true });
+ } catch (err) {
+ if (err instanceof Error && err.message === "UPLOAD_TOO_LARGE") {
+ res.status(413).json({ error: "File too large" });
+ return;
+ }
+ console.error("upload-bytes error:", err);
+ res.status(500).json({ error: "Failed to upload file" });
+ }
+ },
+);
+
+// ─── POST /assets/complete-upload ─────────────────────────────────────────────
+// Called after the browser PUT to R2 succeeds. Marks r2_objects as ready
+// (idempotent — safe when two users upload the same hash concurrently) and
+// finalises the per-user asset record.
+
+app.post("/assets/complete-upload", async (req: Request, res: Response): Promise => {
+ const userId = await getAuthenticatedUserId(req);
+ if (!userId) {
+ res.status(401).json({ error: "Unauthorized" });
+ return;
+ }
+
+ const { assetId, width, height, durationInSeconds } = req.body as {
+ assetId: string;
+ width?: number;
+ height?: number;
+ durationInSeconds?: number;
+ };
+
+ if (!assetId) {
+ res.status(400).json({ error: "assetId is required" });
+ return;
+ }
+
+ try {
+ const { rows: lookup } = await db.query(
+ "SELECT r2_key, content_hash FROM assets WHERE id=$1 AND user_id=$2 AND deleted_at IS NULL",
+ [assetId, userId],
+ );
+
+ if (lookup.length === 0) {
+ res.status(404).json({ error: "Asset not found" });
+ return;
+ }
+
+ const contentHash: string = lookup[0].content_hash;
+
+ // Mark the shared R2 object as confirmed (idempotent — no-op if already ready)
+ await db.query("UPDATE r2_objects SET status='ready' WHERE content_hash=$1 AND status='pending'", [contentHash]);
+
+ const { rows } = await db.query(
+ `UPDATE assets
+ SET status='ready', width=$3, height=$4, duration_seconds=$5
+ WHERE id=$1 AND user_id=$2
+ RETURNING *`,
+ [assetId, userId, width ?? null, height ?? null, durationInSeconds ?? null],
+ );
+
+ console.log(`✅ Upload complete: ${assetId}`);
+ res.json({ asset: { ...rows[0], assetUrl: getAssetUrl(assetId) } });
+ } catch (err) {
+ console.error("complete-upload error:", err);
+ res.status(500).json({ error: "Failed to complete upload" });
+ }
+});
+
+app.get("/storage", async (req: Request, res: Response): Promise => {
+ const userId = await getAuthenticatedUserId(req);
+ if (!userId) {
+ res.status(401).json({ error: "Unauthorized" });
+ return;
+ }
+
+ try {
+ const { rows } = await db.query<{ used_bytes: string }>(
+ `SELECT COALESCE(SUM(file_size), 0)::bigint AS used_bytes
+ FROM assets
+ WHERE user_id = $1
+ AND deleted_at IS NULL
+ AND status = 'ready'`,
+ [userId],
+ );
res.json({
- success: true,
- filename: newFilename,
- originalName: originalName || decodedFilename,
- url: fileUrl,
- fullUrl: fullUrl,
- size: fileStats.size,
- path: destPath,
+ usedBytes: Number(rows[0]?.used_bytes ?? 0),
+ limitBytes: 2 * 1024 * 1024 * 1024,
});
- } catch (error) {
- console.error("Clone error:", error);
- res.status(500).json({ error: "Failed to clone file" });
+ } catch (err) {
+ console.error("storage error:", err);
+ res.status(500).json({ error: "Failed to fetch storage info" });
}
});
-// Delete file endpoint
-app.delete("/media/:filename", (req: Request, res: Response): void => {
+type AssetDeleteRow = {
+ r2_key: string | null;
+ content_hash: string | null;
+ status: string;
+};
+
+async function shouldDeleteR2Object(client: PoolClient, row: AssetDeleteRow): Promise {
+ if (!row.r2_key || row.status !== "ready") return false;
+
+ if (row.content_hash) {
+ await client.query("SELECT 1 FROM r2_objects WHERE content_hash=$1 FOR UPDATE", [row.content_hash]);
+
+ const { rows: refRows } = await client.query<{ count: string }>(
+ `SELECT COUNT(*) AS count FROM assets
+ WHERE content_hash=$1 AND deleted_at IS NULL AND status='ready'`,
+ [row.content_hash],
+ );
+ const remaining = parseInt(refRows[0]?.count ?? "0", 10);
+ if (remaining === 0) {
+ await client.query("DELETE FROM r2_objects WHERE content_hash=$1", [row.content_hash]);
+ return true;
+ }
+ return false;
+ }
+
+ await client.query(
+ `SELECT 1 FROM assets
+ WHERE r2_key=$1 AND deleted_at IS NULL AND status='ready'
+ FOR UPDATE`,
+ [row.r2_key],
+ );
+
+ const { rows: directRefRows } = await client.query<{ count: string }>(
+ `SELECT COUNT(*) AS count FROM assets
+ WHERE r2_key=$1 AND deleted_at IS NULL AND status='ready'`,
+ [row.r2_key],
+ );
+ const remainingDirect = parseInt(directRefRows[0]?.count ?? "0", 10);
+ return remainingDirect === 0;
+}
+
+app.delete("/projects/:projectId", async (req: Request, res: Response): Promise => {
+ const userId = await getAuthenticatedUserId(req);
+ if (!userId) {
+ res.status(401).json({ error: "Unauthorized" });
+ return;
+ }
+
+ const { projectId } = req.params;
+ const client = await db.connect();
+ const r2KeysToDelete = new Set();
+
try {
- const filename = decodeURIComponent(req.params.filename);
- const filePath = path.resolve("out", filename);
+ await client.query("BEGIN");
- // Security check - ensure file is in the out directory
- if (!filePath.startsWith(path.resolve("out"))) {
- res.status(403).json({ error: "Access denied" });
+ const { rows: projectRows } = await client.query("SELECT id FROM projects WHERE id=$1 AND user_id=$2 FOR UPDATE", [
+ projectId,
+ userId,
+ ]);
+ if (projectRows.length === 0) {
+ await client.query("ROLLBACK");
+ res.status(404).json({ error: "Project not found" });
return;
}
- if (!fs.existsSync(filePath)) {
- res.status(404).json({ error: "File not found" });
+ const { rows: assetRows } = await client.query(
+ `SELECT r2_key, content_hash, status
+ FROM assets
+ WHERE project_id=$1 AND user_id=$2 AND deleted_at IS NULL
+ FOR UPDATE`,
+ [projectId, userId],
+ );
+
+ await client.query(
+ `UPDATE assets
+ SET deleted_at = now()
+ WHERE project_id=$1 AND user_id=$2 AND deleted_at IS NULL`,
+ [projectId, userId],
+ );
+
+ const handledHashes = new Set();
+ const handledDirectKeys = new Set();
+
+ for (const assetRow of assetRows) {
+ if (!assetRow.r2_key || assetRow.status !== "ready") continue;
+
+ if (assetRow.content_hash) {
+ if (handledHashes.has(assetRow.content_hash)) continue;
+ handledHashes.add(assetRow.content_hash);
+ } else {
+ if (handledDirectKeys.has(assetRow.r2_key)) continue;
+ handledDirectKeys.add(assetRow.r2_key);
+ }
+
+ const deleteObject = await shouldDeleteR2Object(client, assetRow);
+ if (deleteObject) {
+ r2KeysToDelete.add(assetRow.r2_key);
+ }
+ }
+
+ await client.query("DELETE FROM projects WHERE id=$1 AND user_id=$2", [projectId, userId]);
+ await client.query("COMMIT");
+ } catch (err) {
+ await client.query("ROLLBACK").catch(() => {});
+ console.error("delete project error:", err);
+ res.status(500).json({ error: "Failed to delete project" });
+ return;
+ } finally {
+ client.release();
+ }
+
+ for (const r2Key of r2KeysToDelete) {
+ try {
+ await r2.send(new DeleteObjectCommand({ Bucket: ASSETS_BUCKET, Key: r2Key }));
+ console.log(`🗑️ R2 object removed after project delete: ${r2Key}`);
+ } catch (r2Err) {
+ console.warn(`⚠️ Failed to delete R2 object ${r2Key} after project delete:`, r2Err);
+ }
+ }
+
+ res.status(204).send();
+});
+
+// ─── DELETE /assets/:assetId ──────────────────────────────────────────────────
+// Reference-counted delete.
+// - For deduped assets: object is deleted only when no active row references
+// the same content_hash.
+// - For direct/copy assets (no content_hash): object is deleted only when no
+// active row references the same r2_key.
+
+app.delete("/assets/:assetId", async (req: Request, res: Response): Promise => {
+ const userId = await getAuthenticatedUserId(req);
+ if (!userId) {
+ res.status(401).json({ error: "Unauthorized" });
+ return;
+ }
+
+ const { assetId } = req.params;
+ const client = await db.connect();
+
+ try {
+ await client.query("BEGIN");
+
+ // Fetch and lock the asset row
+ const { rows } = await client.query<{ r2_key: string; content_hash: string | null; status: string }>(
+ "SELECT r2_key, content_hash, status FROM assets WHERE id=$1 AND user_id=$2 AND deleted_at IS NULL FOR UPDATE",
+ [assetId, userId],
+ );
+
+ if (rows.length === 0) {
+ await client.query("ROLLBACK");
+ res.status(404).json({ error: "Asset not found" });
return;
}
- fs.unlinkSync(filePath);
- console.log(`🗑️ File deleted: ${filename}`);
+ const r2Key: string = rows[0].r2_key;
+ const contentHash: string | null = rows[0].content_hash;
+ const assetStatus: string = rows[0].status;
- res.json({
- success: true,
- message: `File ${filename} deleted successfully`,
+ // Soft-delete this user's asset record
+ await client.query("UPDATE assets SET deleted_at=now() WHERE id=$1", [assetId]);
+
+ const shouldDeleteObject = await shouldDeleteR2Object(client, {
+ r2_key: r2Key,
+ content_hash: contentHash,
+ status: assetStatus,
});
- } catch (error) {
- console.error("Delete error:", error);
- res.status(500).json({ error: "Failed to delete file" });
+
+ await client.query("COMMIT");
+
+ // Delete R2 object outside the transaction (network call should not hold a DB lock)
+ if (shouldDeleteObject) {
+ try {
+ await r2.send(new DeleteObjectCommand({ Bucket: ASSETS_BUCKET, Key: r2Key }));
+ console.log(`🗑️ R2 object removed: ${r2Key}`);
+ } catch (r2Err) {
+ // Log but don't fail — DB is already consistent, orphaned R2 objects are benign
+ console.warn(`⚠️ Failed to delete R2 object ${r2Key}:`, r2Err);
+ }
+ }
+
+ console.log(`🗑️ Asset deleted: ${assetId}${shouldDeleteObject ? " (R2 object removed)" : " (shared, kept)"}`);
+ res.json({ success: true });
+ } catch (err) {
+ await client.query("ROLLBACK").catch(() => {});
+ console.error("delete asset error:", err);
+ res.status(500).json({ error: "Failed to delete asset" });
+ } finally {
+ client.release();
}
});
-// Health check endpoint to monitor system resources
-app.get("/health", (req, res) => {
- const used = process.memoryUsage();
- res.json({
- status: "ok",
- memory: {
- rss: `${Math.round(used.rss / 1024 / 1024)} MB`,
- heapTotal: `${Math.round(used.heapTotal / 1024 / 1024)} MB`,
- heapUsed: `${Math.round(used.heapUsed / 1024 / 1024)} MB`,
- },
- uptime: `${Math.round(process.uptime())} seconds`,
- });
-});
+// ─── POST /assets/:assetId/clone ──────────────────────────────────────────────
+// Server-side R2 copy, used for audio splitting.
+
+app.post("/assets/:assetId/clone", async (req: Request, res: Response): Promise => {
+ const userId = await getAuthenticatedUserId(req);
+ if (!userId) {
+ res.status(401).json({ error: "Unauthorized" });
+ return;
+ }
+
+ const { assetId } = req.params;
+ const { suffix } = req.body as { suffix?: string };
-app.post("/render", async (req, res) => {
try {
- // Get input props from POST body
- const inputProps = {
- timelineData: req.body.timelineData,
- durationInFrames: req.body.durationInFrames,
- compositionWidth: req.body.compositionWidth,
- compositionHeight: req.body.compositionHeight,
- getPixelsPerSecond: req.body.getPixelsPerSecond,
- isRendering: true,
- };
+ const { rows } = await db.query("SELECT * FROM assets WHERE id=$1 AND user_id=$2 AND deleted_at IS NULL", [
+ assetId,
+ userId,
+ ]);
- // console.log("Input props:", typeof inputProps.compositionWidth);
- console.log("Input props:", JSON.stringify(inputProps, null, 2));
- // Get the composition you want to render
- const composition = await selectComposition({
- serveUrl: bundleLocation,
- id: compositionId,
- inputProps,
+ if (rows.length === 0) {
+ res.status(404).json({ error: "Source asset not found" });
+ return;
+ }
+
+ const source = rows[0];
+ const sourceKey: string = source.r2_key;
+ const ext = path.extname(sourceKey);
+ const newAssetId = generateUUID();
+ const newR2Key = `${userId}/${newAssetId}${ext}`;
+
+ await r2.send(
+ new CopyObjectCommand({
+ CopySource: `${ASSETS_BUCKET}/${sourceKey}`,
+ Bucket: ASSETS_BUCKET,
+ Key: newR2Key,
+ }),
+ );
+
+ const newFilename = suffix ? `${path.basename(source.filename, ext)} ${suffix}${ext}` : source.filename;
+
+ const { rows: newRows } = await db.query(
+ `INSERT INTO assets
+ (id, user_id, project_id, r2_key, filename, file_size, mime_type,
+ media_type, duration_seconds, width, height, status)
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,'ready')
+ RETURNING *`,
+ [
+ newAssetId,
+ userId,
+ source.project_id,
+ newR2Key,
+ newFilename,
+ source.file_size,
+ source.mime_type,
+ source.media_type,
+ source.duration_seconds,
+ source.width,
+ source.height,
+ ],
+ );
+ // Note: cloned assets intentionally have no content_hash since they represent
+ // a new physical object in R2 (e.g. audio extracted from video).
+
+ console.log(`📋 Asset cloned: ${assetId} → ${newAssetId}`);
+ res.json({ asset: { ...newRows[0], assetUrl: getAssetUrl(newAssetId) } });
+ } catch (err) {
+ console.error("clone asset error:", err);
+ res.status(500).json({ error: "Failed to clone asset" });
+ }
+});
+
+// ─── POST /render ──────────────────────────────────────────────────────────────
+// Enqueues a render job and returns { jobId } immediately.
+// Client opens GET /render/:jobId/events for SSE progress updates.
+
+app.post("/render", async (req: Request, res: Response): Promise => {
+ const userId = await getAuthenticatedUserId(req);
+ if (!userId) {
+ res.status(401).json({ error: "Unauthorized" });
+ return;
+ }
+
+ const projectId = typeof req.body.projectId === "string" ? req.body.projectId : "";
+ if (!UUID_PATTERN.test(projectId)) {
+ res.status(400).json({ error: "Valid projectId is required" });
+ return;
+ }
+ if (!(await assertProjectOwned(userId, projectId))) {
+ res.status(404).json({ error: "Project not found" });
+ return;
+ }
+
+ const renderJobId = generateUUID();
+
+ const VALID_CODECS = new Set(["h264", "h265", "vp9"]);
+ const codec = VALID_CODECS.has(req.body.codec) ? req.body.codec : "h264";
+ const advancedMode = req.body.advancedMode === true;
+ const crf = typeof req.body.crf === "number"
+ ? clampExportCrf(req.body.crf, advancedMode)
+ : 28;
+ const VALID_PRESETS = new Set(["1080p", "720p", "source", "4k"]);
+ const resolutionPreset: ExportResolutionPreset = VALID_PRESETS.has(req.body.resolutionPreset)
+ ? req.body.resolutionPreset
+ : "1080p";
+ const codecExt = extForCodec(codec);
+ const outputFileName =
+ typeof req.body.outputFileName === "string" && req.body.outputFileName.trim()
+ ? sanitizeExportFileName(req.body.outputFileName, codecExt)
+ : sanitizeExportFileName("export", codecExt);
+
+ let jpegQuality: number | undefined;
+ if (typeof req.body.jpegQuality === "number") {
+ jpegQuality = Math.min(100, Math.max(60, Math.round(req.body.jpegQuality)));
+ }
+
+ let x264Preset: X264Preset | undefined;
+ if (
+ typeof req.body.x264Preset === "string" &&
+ (X264_PRESETS as readonly string[]).includes(req.body.x264Preset)
+ ) {
+ x264Preset = req.body.x264Preset as X264Preset;
+ }
+
+ const muted = req.body.muted === true;
+
+ const compositionWidth = Number(req.body.compositionWidth) || 1920;
+ const compositionHeight = Number(req.body.compositionHeight) || 1080;
+ const durationInFrames = Number(req.body.durationInFrames) || 30;
+ const getPixelsPerSecond = Number(req.body.getPixelsPerSecond) || 100;
+
+ const cappedForFingerprint = capExportDimensions(
+ compositionWidth,
+ compositionHeight,
+ resolutionPreset,
+ );
+ const tuningDefaults = getRemotionRenderTuning(
+ cappedForFingerprint.width,
+ cappedForFingerprint.height,
+ );
+ const jpegForFingerprint =
+ typeof jpegQuality === "number" ? jpegQuality : tuningDefaults.jpegQuality;
+ const x264ForFingerprint =
+ codec === "h264" ? (x264Preset ?? tuningDefaults.x264Preset) : undefined;
+
+ const contentFingerprint = computeExportFingerprint({
+ timelineData: req.body.timelineData,
+ durationInFrames,
+ compositionWidth,
+ compositionHeight,
+ codec,
+ crf,
+ resolutionPreset,
+ muted,
+ jpegQuality: jpegForFingerprint,
+ x264Preset: x264ForFingerprint,
+ });
+
+ const cached = await findCachedRender(projectId, userId, contentFingerprint);
+ if (cached) {
+ const urls = await signRenderAssetUrls(cached);
+ console.log(`♻️ Serving cached render for project ${projectId}`);
+ res.json({
+ cached: true,
+ renderId: cached.id,
+ fileName: cached.file_name,
+ ...urls,
});
+ return;
+ }
- // const maxFrames = Math.min(composition.durationInFrames, 150); // Max 5 seconds at 30fps
- // console.log(`Starting ULTRA low-resource render. Limiting to ${maxFrames} frames (${maxFrames / 30}s)`);
+ const inputProps = {
+ timelineData: req.body.timelineData,
+ durationInFrames,
+ compositionWidth: cappedForFingerprint.width,
+ compositionHeight: cappedForFingerprint.height,
+ getPixelsPerSecond,
+ isRendering: true,
+ };
- // Render optimized for 4vCPU, 8GB RAM server
- await renderMedia({
- composition,
- serveUrl: bundleLocation,
- codec: "h264",
- outputLocation: `out/${compositionId}.mp4`,
+ try {
+ const job = await renderQueue.add("render", {
+ userId,
+ projectId,
+ renderJobId,
+ contentFingerprint,
+ codec,
+ crf,
+ resolutionPreset,
+ outputFileName,
+ advancedMode,
+ jpegQuality,
+ x264Preset,
+ muted,
inputProps,
- // Optimized settings for server hardware
- concurrency: 3, // Use 3 cores, leave 1 for system
- verbose: true,
- logLevel: "info", // More detailed logging for server monitoring
- // Balanced encoding settings for server performance
- ffmpegOverride: ({ args }) => {
- return [
- ...args,
- "-preset",
- "fast", // Good balance of speed and quality
- "-crf",
- "28", // Better quality than ultrafast setting
- "-threads",
- "3", // Use 3 threads for encoding
- "-tune",
- "film", // Better quality for general content
- "-x264-params",
- "ref=3:me=hex:subme=6:trellis=1", // Better quality settings
- "-g",
- "30", // Standard keyframe interval
- "-bf",
- "2", // Allow some B-frames for better compression
- "-maxrate",
- "5M", // Limit bitrate to prevent memory issues
- "-bufsize",
- "10M", // Buffer size for rate control
- ];
- },
- timeoutInMilliseconds: 900000, // 15 minute timeout for longer videos
});
+ console.log(`📬 Render job queued: ${job.id}`);
+ res.json({ cached: false, jobId: job.id });
+ } catch (err) {
+ console.error("❌ Failed to enqueue render:", err);
+ res.status(500).json({ error: "Failed to queue render job" });
+ }
+});
+
+// ─── GET /projects/:projectId/renders — export history ───────────────────────
+
+app.get("/projects/:projectId/renders", async (req: Request, res: Response): Promise => {
+ const userId = await getAuthenticatedUserId(req);
+ if (!userId) {
+ res.status(401).json({ error: "Unauthorized" });
+ return;
+ }
- console.log("✅ Render completed successfully");
- res.sendFile(path.resolve(`out/${compositionId}.mp4`));
+ const { projectId } = req.params;
+ if (!UUID_PATTERN.test(projectId)) {
+ res.status(400).json({ error: "Invalid project id" });
+ return;
+ }
+ if (!(await assertProjectOwned(userId, projectId))) {
+ res.status(404).json({ error: "Project not found" });
+ return;
+ }
+
+ try {
+ const { rows } = await db.query(
+ `SELECT id, file_name, codec, width, height, r2_video_key, r2_thumb_key, created_at
+ FROM project_renders
+ WHERE project_id = $1::uuid AND user_id = $2
+ ORDER BY created_at DESC
+ LIMIT 50`,
+ [projectId, userId],
+ );
+
+ const renders = await Promise.all(
+ rows.map(async (row) => {
+ const urls = await signRenderAssetUrls({
+ file_name: row.file_name as string,
+ r2_video_key: row.r2_video_key as string,
+ r2_thumb_key: (row.r2_thumb_key as string | null) ?? null,
+ codec: row.codec as string,
+ });
+ return {
+ id: String(row.id),
+ fileName: row.file_name as string,
+ codec: row.codec as string,
+ width: row.width as number,
+ height: row.height as number,
+ createdAt: (row.created_at as Date).toISOString(),
+ thumbnailUrl: urls.thumbnailUrl,
+ previewUrl: urls.previewUrl,
+ downloadUrl: urls.downloadUrl,
+ };
+ }),
+ );
+
+ res.json({ renders });
} catch (err) {
- console.error("❌ Render failed:", err);
+ console.error("export history error:", err);
+ res.status(500).json({ error: "Failed to load export history" });
+ }
+});
+
+// ─── DELETE /projects/:projectId/renders/:renderId ───────────────────────────
+
+app.delete(
+ "/projects/:projectId/renders/:renderId",
+ async (req: Request, res: Response): Promise => {
+ const userId = await getAuthenticatedUserId(req);
+ if (!userId) {
+ res.status(401).json({ error: "Unauthorized" });
+ return;
+ }
+
+ const { projectId, renderId } = req.params;
+ if (!UUID_PATTERN.test(projectId) || !UUID_PATTERN.test(renderId)) {
+ res.status(400).json({ error: "Invalid id" });
+ return;
+ }
+ if (!(await assertProjectOwned(userId, projectId))) {
+ res.status(404).json({ error: "Project not found" });
+ return;
+ }
- // Clean up failed renders
try {
- const outputPath = `out/${compositionId}.mp4`;
- if (fs.existsSync(outputPath)) {
- fs.unlinkSync(outputPath);
- console.log("🧹 Cleaned up partial file");
+ const { rows } = await db.query(
+ `SELECT r2_video_key, r2_thumb_key
+ FROM project_renders
+ WHERE id = $1::uuid AND project_id = $2::uuid AND user_id = $3`,
+ [renderId, projectId, userId],
+ );
+ if (rows.length === 0) {
+ res.status(404).json({ error: "Export not found" });
+ return;
+ }
+
+ const videoKey = rows[0].r2_video_key as string;
+ const thumbKey = (rows[0].r2_thumb_key as string | null) ?? null;
+
+ await db.query(
+ `DELETE FROM project_renders
+ WHERE id = $1::uuid AND project_id = $2::uuid AND user_id = $3`,
+ [renderId, projectId, userId],
+ );
+
+ try {
+ await deleteRenderR2Objects(userId, videoKey, thumbKey);
+ console.log(`🗑️ Export deleted: ${renderId} (${videoKey})`);
+ } catch (r2Err) {
+ console.warn(`⚠️ Export removed from DB but R2 delete failed for ${renderId}:`, r2Err);
}
- } catch (cleanupErr) {
- console.warn("⚠️ Could not clean up:", cleanupErr);
+
+ res.status(204).send();
+ } catch (err) {
+ console.error("delete export error:", err);
+ res.status(500).json({ error: "Failed to delete export" });
}
+ },
+);
- res.status(500).json({
- error: "Video rendering failed",
- message: "Your laptop might be under heavy load. Try closing other apps and rendering again.",
- tip: "Videos are limited to 5 seconds at half resolution for performance.",
- });
+// ─── GET /render/:jobId/events ─────────────────────────────────────────────────
+// SSE stream of render progress. Server pushes events — no client polling.
+
+app.get("/render/:jobId/events", async (req: Request, res: Response): Promise => {
+ const userId = await getAuthenticatedUserId(req);
+ if (!userId) {
+ res.status(401).end();
+ return;
}
+
+ const { jobId } = req.params;
+
+ res.setHeader("Content-Type", "text/event-stream");
+ res.setHeader("Cache-Control", "no-cache");
+ res.setHeader("Connection", "keep-alive");
+ res.setHeader("X-Accel-Buffering", "no");
+ res.flushHeaders();
+
+ const send = (data: object) => {
+ if (!res.writableEnded) res.write(`data: ${JSON.stringify(data)}\n\n`);
+ };
+
+ const heartbeat = setInterval(() => {
+ if (!res.writableEnded) res.write(": heartbeat\n\n");
+ }, 30_000);
+
+ // Handle reconnects: check if job already finished
+ const job = await Job.fromId(renderQueue, jobId);
+ if (!job) {
+ send({ type: "error", message: "Job not found" });
+ clearInterval(heartbeat);
+ res.end();
+ return;
+ }
+
+ const state = await job.getState();
+ if (state === "completed") {
+ const rv = parseRenderJobReturn(job.returnvalue);
+ if (rv?.downloadUrl) {
+ send({ type: "completed", downloadUrl: rv.downloadUrl, fileName: rv.fileName });
+ } else {
+ send({ type: "error", message: "Render finished but download URL is missing" });
+ }
+ clearInterval(heartbeat);
+ res.end();
+ return;
+ }
+ if (state === "failed") {
+ send({ type: "failed", message: job.failedReason ?? "Render failed" });
+ clearInterval(heartbeat);
+ res.end();
+ return;
+ }
+
+ const onProgress = ({ jobId: jId, data }: { jobId: string; data: unknown }) => {
+ if (jId !== jobId) return;
+ send({ type: "progress", percent: typeof data === "number" ? data : 0 });
+ };
+
+ const onCompleted = ({ jobId: jId, returnvalue }: { jobId: string; returnvalue: unknown }) => {
+ if (jId !== jobId) return;
+ const rv = parseRenderJobReturn(returnvalue);
+ if (rv?.downloadUrl) {
+ send({ type: "completed", downloadUrl: rv.downloadUrl, fileName: rv.fileName });
+ } else {
+ console.error("Render completed but return value was invalid:", returnvalue);
+ send({ type: "error", message: "Render finished but download URL is missing" });
+ }
+ cleanup();
+ res.end();
+ };
+
+ const onFailed = ({ jobId: jId, failedReason }: { jobId: string; failedReason: string }) => {
+ if (jId !== jobId) return;
+ send({ type: "failed", message: failedReason ?? "Render failed" });
+ cleanup();
+ res.end();
+ };
+
+ const cleanup = () => {
+ clearInterval(heartbeat);
+ renderQueueEvents.off("progress", onProgress);
+ renderQueueEvents.off("completed", onCompleted);
+ renderQueueEvents.off("failed", onFailed);
+ };
+
+ renderQueueEvents.on("progress", onProgress);
+ renderQueueEvents.on("completed", onCompleted);
+ renderQueueEvents.on("failed", onFailed);
+ req.on("close", cleanup);
});
+// ─── Start ────────────────────────────────────────────────────────────────────
+
const port = process.env.PORT || 8000;
-app.listen(port, () => {
- console.log(`🚀 Server running on http://localhost:${port}`);
- console.log(`📊 Health check: http://localhost:${port}/health`);
- console.log(`🎬 Video rendering: POST http://localhost:${port}/render`);
- console.log(`📁 Media files: http://localhost:${port}/media/`);
- console.log(`📤 Upload file: POST http://localhost:${port}/upload`);
- console.log(`📤 Upload multiple: POST http://localhost:${port}/upload-multiple`);
- console.log(`📋 Clone media: POST http://localhost:${port}/clone-media`);
- console.log(`🗑️ Delete file: DELETE http://localhost:${port}/media/:filename`);
- console.log(`🖥️ Optimized for 4vCPU, 8GB RAM server:`);
- console.log(` - Multi-threaded processing (3 cores)`);
- console.log(` - Balanced quality/speed encoding`);
- console.log(` - Full resolution rendering`);
- console.log(` - 15-minute timeout for longer videos`);
- console.log(`📂 Media files are served from: ${path.resolve("out")}`);
-});
+void ensureProjectRendersTable()
+ .then(() => {
+ app.listen(port, () => {
+ console.log(`🚀 Render server listening on http://localhost:${port}`);
+ console.log(`☁️ Assets bucket: ${ASSETS_BUCKET || "(not set)"}`);
+ console.log(`☁️ Renders bucket: ${RENDERS_BUCKET || "(not set)"}`);
+ });
+ })
+ .catch((err) => {
+ console.error("Failed to initialize project_renders table:", err);
+ process.exit(1);
+ });
diff --git a/backend/Dockerfile b/backend/Dockerfile
index 0a6fcbab..8ffa7a83 100644
--- a/backend/Dockerfile
+++ b/backend/Dockerfile
@@ -1,15 +1,23 @@
-FROM python:3.12-slim
+FROM python:3.12.7-slim
-RUN pip install uv
+# curl for healthchecks; uv installed via pip.
+RUN apt-get update && apt-get install -y --no-install-recommends curl \
+ && rm -rf /var/lib/apt/lists/* \
+ && pip install --no-cache-dir uv
WORKDIR /app
COPY pyproject.toml uv.lock ./
-RUN uv sync --frozen
+RUN uv sync --frozen --no-dev
COPY . .
+# Run as a non-root user.
+RUN useradd --system --uid 1001 --create-home appuser \
+ && chown -R appuser:appuser /app
+USER appuser
+
EXPOSE 3000
CMD ["uv", "run", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "3000"]
diff --git a/backend/ai/routes.py b/backend/ai/routes.py
index 44585fff..9020bed6 100644
--- a/backend/ai/routes.py
+++ b/backend/ai/routes.py
@@ -1,68 +1,216 @@
+import json
+import logging
+import asyncio
from typing import Any
-from fastapi import APIRouter, HTTPException
+from fastapi import APIRouter, Depends, HTTPException, status
from google import genai
-from pydantic import BaseModel, ConfigDict
+from pydantic import BaseModel, ConfigDict, Field
from ai.schema import FunctionCallResponse
+from auth.routes import get_current_user
+from auth.schema import SessionUser
+from db import get_db_pool
from utils import require_env
+logger = logging.getLogger(__name__)
+
router = APIRouter(tags=["ai"])
-GEMINI_API_KEY: str = require_env("GEMINI_API_KEY")
+_GEMINI_MODEL = "gemini-2.5-flash"
+GEMINI_API_KEY: str = require_env("GEMINI_API_KEY")
gemini_client: genai.Client = genai.Client(api_key=GEMINI_API_KEY)
+_MAX_MESSAGE_LENGTH = 20_000
+_MAX_HISTORY_ITEMS = 50
+# Permissive caps — large enough for real projects (hundreds of scrubbers / media items) without
+# hitting Gemini 2.5 Flash's ~1M-token context window. Tune when we have load data.
+_MAX_TIMELINE_BYTES = 10 * 1024 * 1024 # 10 MB
+_MAX_MEDIABIN_BYTES = 4 * 1024 * 1024 # 4 MB
+
+# Per-user DB-backed rate limit. This is shared across worker processes and instances.
+# For higher scale, replace with Redis.
+_RATE_LIMIT_WINDOW_SECONDS = 60
+_RATE_LIMIT_MAX_REQUESTS = 60
+_RATE_LIMIT_TABLE = "ai_rate_limit_events"
+_rate_limit_ready = False
+_rate_limit_init_lock = asyncio.Lock()
+
+
+async def _ensure_rate_limit_table() -> None:
+ global _rate_limit_ready
+ if _rate_limit_ready:
+ return
+ async with _rate_limit_init_lock:
+ if _rate_limit_ready:
+ return
+ pool = await get_db_pool()
+ async with pool.acquire() as conn:
+ await conn.execute(
+ f"""
+ CREATE TABLE IF NOT EXISTS {_RATE_LIMIT_TABLE} (
+ user_id TEXT NOT NULL,
+ occurred_at TIMESTAMPTZ NOT NULL DEFAULT now()
+ )
+ """
+ )
+ await conn.execute(
+ f"""
+ CREATE INDEX IF NOT EXISTS idx_{_RATE_LIMIT_TABLE}_user_time
+ ON {_RATE_LIMIT_TABLE} (user_id, occurred_at)
+ """
+ )
+ await conn.execute(
+ f"""
+ CREATE INDEX IF NOT EXISTS idx_{_RATE_LIMIT_TABLE}_time
+ ON {_RATE_LIMIT_TABLE} (occurred_at)
+ """
+ )
+ _rate_limit_ready = True
+
+
+async def _enforce_rate_limit(user_id: str) -> None:
+ await _ensure_rate_limit_table()
+ pool = await get_db_pool()
+ async with pool.acquire() as conn:
+ async with conn.transaction():
+ await conn.execute("SELECT pg_advisory_xact_lock(hashtext($1))", user_id)
+ await conn.execute(
+ f"""
+ DELETE FROM {_RATE_LIMIT_TABLE}
+ WHERE occurred_at < now() - make_interval(secs => $1::int)
+ """,
+ _RATE_LIMIT_WINDOW_SECONDS,
+ )
+ count_row = await conn.fetchrow(
+ f"SELECT COUNT(*)::int AS request_count FROM {_RATE_LIMIT_TABLE} WHERE user_id = $1",
+ user_id,
+ )
+ request_count = int(count_row["request_count"]) if count_row else 0
+ if request_count >= _RATE_LIMIT_MAX_REQUESTS:
+ raise HTTPException(
+ status_code=status.HTTP_429_TOO_MANY_REQUESTS,
+ detail=f"Rate limit exceeded: {_RATE_LIMIT_MAX_REQUESTS} requests per minute",
+ )
+ await conn.execute(
+ f"INSERT INTO {_RATE_LIMIT_TABLE} (user_id) VALUES ($1)",
+ user_id,
+ )
+
class Message(BaseModel):
- # Be permissive with incoming payloads from the frontend
model_config = ConfigDict(extra="ignore")
- message: str # the full user message
- mentioned_scrubber_ids: list[str] | None = None # scrubber ids mentioned via '@'
- # Accept any shape for resilience; backend does not mutate these
- timeline_state: dict[str, Any] | None = None # current timeline state
- mediabin_items: list[dict[str, Any]] | None = None # current media bin
- chat_history: list[dict[str, Any]] | None = (
- None # prior turns: [{"role":"user"|"assistant","content":"..."}]
- )
+ message: str = Field(max_length=_MAX_MESSAGE_LENGTH)
+ mentioned_scrubber_ids: list[str] | None = None
+ timeline_state: dict[str, Any] | None = None
+ mediabin_items: list[dict[str, Any]] | None = None
+ chat_history: list[dict[str, Any]] | None = None
@router.post("/ai")
-async def process_ai_message(request: Message) -> FunctionCallResponse:
+async def process_ai_message(
+ request: Message,
+ user: SessionUser = Depends(get_current_user),
+) -> FunctionCallResponse:
+ await _enforce_rate_limit(user.user_id)
+
+ # Bound the serialized payload before forwarding to Gemini to cap token spend.
+ timeline_json = json.dumps(request.timeline_state or {}, ensure_ascii=False)
+ if len(timeline_json) > _MAX_TIMELINE_BYTES:
+ raise HTTPException(
+ status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
+ detail="Timeline state too large",
+ )
+ mediabin_json = json.dumps(request.mediabin_items or [], ensure_ascii=False)
+ if len(mediabin_json) > _MAX_MEDIABIN_BYTES:
+ raise HTTPException(
+ status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
+ detail="Media bin too large",
+ )
+
+ # Truncate history to last N items to limit prompt size
+ history = (request.chat_history or [])[-_MAX_HISTORY_ITEMS:]
+
+ prompt = f"""
+You are Kimu, an AI video-editing assistant.
+
+## Response rules
+- Call ONE tool when the user explicitly requests an editing action.
+- Set function_call to null and return a short assistant_message for greetings, questions, or when the action is ambiguous.
+- Never guess IDs — use scrubber_name or look them up in the timeline/media-bin data provided.
+- Always use pixels_per_second=100 unless the user states otherwise.
+- Tracks are 1-based: "track 1" → track_number=1, track_id="track-1".
+
+## Tool catalogue
+
+### Adding clips
+- **LLMAddScrubberByName** — preferred when user says a clip name, track, and time.
+ Args: scrubber_name (substring match), track_number (1-based), position_seconds, pixels_per_second=100
+- **LLMAddScrubberToTimeline** — when you have the exact media-bin item id.
+ Args: scrubber_id (exact), track_id ("track-N"), drop_left_px
+
+### Moving & resizing
+- **LLMMoveScrubber** — move a clip to a new time/track.
+ Args: scrubber_id, new_position_seconds, new_track_number, pixels_per_second=100
+- **LLMResizeScrubber** — change a clip's displayed duration.
+ Args: scrubber_id (or scrubber_name+track_number), new_duration_seconds, pixels_per_second=100
+- **LLMMoveScrubbersByOffset** — shift multiple clips forward/back by a delta.
+ Args: scrubber_ids (list), offset_seconds (positive=right, negative=left), pixels_per_second=100
+
+### Deleting
+- **LLMDeleteScrubber** — remove one clip.
+ Args: scrubber_id (or scrubber_name)
+- **LLMDeleteScrubbersInTrack** — clear all clips from a track.
+ Args: track_number
+
+### Splitting
+- **LLMSplitScrubber** — split a clip into two at a specific timeline time.
+ Args: scrubber_id (or scrubber_name), time_seconds (absolute timeline position)
+
+### Tracks
+- **LLMCreateTrack** — add new empty track(s).
+ Args: count=1
+
+### Audio / video properties
+- **LLMSetVolume** — set volume or mute for an audio/video clip.
+ Args: scrubber_id (or scrubber_name), volume (0.0–1.0), muted=false
+- **LLMSetPlaybackSpeed** — change playback speed. Allowed values: 0.25, 0.5, 1, 1.5, 2, 4.
+ Args: scrubber_id (or scrubber_name), playback_rate
+
+### Text clips
+- **LLMUpdateTextContent** — change the text in a text clip.
+ Args: scrubber_id, new_text_content
+- **LLMUpdateTextStyle** — change font/size/colour/alignment.
+ Args: scrubber_id, fontSize (px), fontFamily, color (hex), textAlign (left/center/right), fontWeight (normal/bold)
+
+## Context
+Conversation history (oldest first): {json.dumps(history, ensure_ascii=False)}
+User message: {json.dumps(request.message, ensure_ascii=False)}
+Mentioned clip IDs: {json.dumps(request.mentioned_scrubber_ids or [])}
+Timeline state: {timeline_json}
+Media bin: {mediabin_json}
+"""
+
try:
response = gemini_client.models.generate_content(
- model="gemini-2.5-flash",
- contents=f"""
- You are Kimu, an AI assistant inside a video editor. You can decide to either:
- - call ONE tool from the provided schema when the user explicitly asks for an editing action, or
- - return a short friendly assistant_message when no concrete action is needed (e.g., greetings, small talk, clarifying questions).
-
- Strictly follow:
- - If the user's message does not clearly request an editing action, set function_call to null and include an assistant_message.
- - Only produce a function_call when it is safe and unambiguous to execute.
-
- Inference rules:
- - Assume a single active timeline; do NOT require a timeline_id.
- - Tracks are named like "track-1", but when the user says "track 1" they mean number 1.
- - Use pixels_per_second=100 by default if not provided.
- - When the user names media like "twitter" or "twitter header", map that to the closest media in the media bin by name substring match.
- - Prefer LLMAddScrubberByName when the user specifies a name, track number, and time in seconds.
- - If the user asks to remove scrubbers in a specific track, call LLMDeleteScrubbersInTrack with that track number.
-
- Conversation so far (oldest first): {request.chat_history}
-
- User message: {request.message}
- Mentioned scrubber ids: {request.mentioned_scrubber_ids}
- Timeline state: {request.timeline_state}
- Media bin items: {request.mediabin_items}
- """,
+ model=_GEMINI_MODEL,
+ contents=prompt,
config={
"response_mime_type": "application/json",
"response_schema": FunctionCallResponse,
},
)
-
return FunctionCallResponse.model_validate(response.parsed)
- except Exception as e:
- raise HTTPException(status_code=500, detail=str(e)) from e
+ except ValueError as exc:
+ # Don't include user content (timeline / messages) in logs — log the type only.
+ logger.warning("AI response validation failed: %s", type(exc).__name__)
+ raise HTTPException(
+ status_code=422, detail="Invalid response from AI model"
+ ) from exc
+ except Exception as exc:
+ logger.exception("Unexpected error in AI endpoint for user %s", user.user_id)
+ raise HTTPException(
+ status_code=500, detail="AI service temporarily unavailable"
+ ) from exc
diff --git a/backend/ai/schema.py b/backend/ai/schema.py
index 10cd5beb..18b9d69c 100644
--- a/backend/ai/schema.py
+++ b/backend/ai/schema.py
@@ -66,60 +66,143 @@ class TimelineState(BaseSchema):
tracks: list[TrackState] = Field(description="List of tracks in the timeline")
+# ── Tool argument classes ────────────────────────────────────────────────────
+
+
class LLMAddScrubberToTimelineArgs(BaseSchema):
function_name: Literal["LLMAddScrubberToTimeline"] = Field(
- description="The name of the function to call"
+ description="Add a media-bin item to the timeline using its exact ID"
)
- scrubber_id: str = Field(
- description="The id of the scrubber to add to the timeline"
+ scrubber_id: str = Field(description="Exact media-bin item id to place")
+ track_id: str = Field(description="Track id (e.g. 'track-1')")
+ drop_left_px: int = Field(description="Left pixel offset on the timeline")
+
+
+class LLMAddScrubberByNameArgs(BaseSchema):
+ function_name: Literal["LLMAddScrubberByName"] = Field(
+ description="Add a media-bin item to the timeline by matching its name"
)
- track_id: str = Field(description="The id of the track to add the scrubber to")
- drop_left_px: int = Field(description="The left position of the scrubber in pixels")
+ scrubber_name: str = Field(description="Partial or full name of the media to add")
+ track_number: int = Field(description="1-based track number")
+ position_seconds: float = Field(description="Start time in seconds")
+ pixels_per_second: int = Field(default=100, description="Pixels per second")
class LLMMoveScrubberArgs(BaseSchema):
function_name: Literal["LLMMoveScrubber"] = Field(
- description="The name of the function to call"
+ description="Move a clip to a new time position and/or track"
)
- scrubber_id: str = Field(description="The id of the scrubber to move")
- new_position_seconds: float = Field(
- description="The new position of the scrubber in seconds"
+ scrubber_id: str = Field(description="ID of the scrubber to move")
+ new_position_seconds: float = Field(description="New start time in seconds")
+ new_track_number: int = Field(description="1-based destination track number")
+ pixels_per_second: int = Field(default=100, description="Pixels per second")
+
+
+class LLMResizeScrubberArgs(BaseSchema):
+ function_name: Literal["LLMResizeScrubber"] = Field(
+ description="Change the displayed duration of a clip on the timeline"
)
- new_track_number: int = Field(description="The new track number of the scrubber")
- pixels_per_second: int = Field(description="The number of pixels per second")
+ scrubber_id: str | None = Field(default=None, description="ID of the clip to resize (prefer over name)")
+ scrubber_name: str | None = Field(default=None, description="Name substring to find the clip if id unknown")
+ track_number: int | None = Field(default=None, description="1-based track to search when id/name not given")
+ new_duration_seconds: float = Field(description="New duration in seconds")
+ pixels_per_second: int = Field(default=100, description="Pixels per second")
-class LLMAddScrubberByNameArgs(BaseSchema):
- function_name: Literal["LLMAddScrubberByName"] = Field(
- description="The name of the function to call"
+class LLMDeleteScrubberArgs(BaseSchema):
+ function_name: Literal["LLMDeleteScrubber"] = Field(
+ description="Remove a single clip from the timeline"
)
- scrubber_name: str = Field(
- description="The partial or full name of the media to add"
+ scrubber_id: str | None = Field(default=None, description="ID of the clip to delete")
+ scrubber_name: str | None = Field(default=None, description="Name substring to find the clip if id unknown")
+
+
+class LLMDeleteScrubbersInTrackArgs(BaseSchema):
+ function_name: Literal["LLMDeleteScrubbersInTrack"] = Field(
+ description="Remove ALL clips from a specific track"
)
- track_number: int = Field(description="1-based track number to add to")
- position_seconds: float = Field(
- description="Timeline time in seconds to place the media at"
+ track_number: int = Field(description="1-based track number to clear")
+
+
+class LLMSetVolumeArgs(BaseSchema):
+ function_name: Literal["LLMSetVolume"] = Field(
+ description="Set the volume or mute state of an audio/video clip"
)
- pixels_per_second: int = Field(
- description="Pixels per second to convert time to pixels"
+ scrubber_id: str | None = Field(default=None, description="ID of the clip")
+ scrubber_name: str | None = Field(default=None, description="Name substring to find the clip")
+ volume: float = Field(ge=0.0, le=1.0, description="Volume level 0.0 (silent) to 1.0 (full)")
+ muted: bool = Field(default=False, description="Whether to mute the clip")
+
+
+class LLMSetPlaybackSpeedArgs(BaseSchema):
+ function_name: Literal["LLMSetPlaybackSpeed"] = Field(
+ description="Set the playback speed of a clip (0.25×, 0.5×, 1×, 1.5×, 2×, 4×)"
)
+ scrubber_id: str | None = Field(default=None, description="ID of the clip")
+ scrubber_name: str | None = Field(default=None, description="Name substring to find the clip")
+ playback_rate: float = Field(description="Speed multiplier: 0.25, 0.5, 1, 1.5, 2, or 4")
-class LLMDeleteScrubbersInTrackArgs(BaseSchema):
- function_name: Literal["LLMDeleteScrubbersInTrack"] = Field(
- description="The name of the function to call"
+class LLMSplitScrubberArgs(BaseSchema):
+ function_name: Literal["LLMSplitScrubber"] = Field(
+ description="Split a clip into two at a given time position"
+ )
+ scrubber_id: str | None = Field(default=None, description="ID of the clip to split")
+ scrubber_name: str | None = Field(default=None, description="Name substring to find the clip")
+ time_seconds: float = Field(description="Absolute timeline time (in seconds) to split at")
+
+
+class LLMCreateTrackArgs(BaseSchema):
+ function_name: Literal["LLMCreateTrack"] = Field(
+ description="Add one or more new empty tracks to the timeline"
+ )
+ count: int = Field(default=1, description="Number of tracks to create")
+
+
+class LLMMoveScrubbersByOffsetArgs(BaseSchema):
+ function_name: Literal["LLMMoveScrubbersByOffset"] = Field(
+ description="Shift multiple clips forward or backward by a time offset"
+ )
+ scrubber_ids: list[str] = Field(description="IDs of the clips to shift")
+ offset_seconds: float = Field(description="Seconds to shift (positive = right, negative = left)")
+ pixels_per_second: int = Field(default=100, description="Pixels per second")
+
+
+class LLMUpdateTextContentArgs(BaseSchema):
+ function_name: Literal["LLMUpdateTextContent"] = Field(
+ description="Change the text displayed in a text clip"
)
- track_number: int = Field(
- description="1-based track number whose scrubbers will be removed"
+ scrubber_id: str = Field(description="ID of the text clip")
+ new_text_content: str = Field(description="New text to display")
+
+
+class LLMUpdateTextStyleArgs(BaseSchema):
+ function_name: Literal["LLMUpdateTextStyle"] = Field(
+ description="Change font, size, colour, or alignment of a text clip"
)
+ scrubber_id: str = Field(description="ID of the text clip")
+ fontSize: int | None = Field(default=None, description="Font size in pixels")
+ fontFamily: str | None = Field(default=None, description="Font family name")
+ color: str | None = Field(default=None, description="Colour as hex string, e.g. '#ff0000'")
+ textAlign: Literal["left", "center", "right"] | None = Field(default=None, description="Text alignment")
+ fontWeight: Literal["normal", "bold"] | None = Field(default=None, description="Font weight")
class FunctionCallResponse(BaseSchema):
function_call: (
LLMAddScrubberToTimelineArgs
- | LLMMoveScrubberArgs
| LLMAddScrubberByNameArgs
+ | LLMMoveScrubberArgs
+ | LLMResizeScrubberArgs
+ | LLMDeleteScrubberArgs
| LLMDeleteScrubbersInTrackArgs
+ | LLMSetVolumeArgs
+ | LLMSetPlaybackSpeedArgs
+ | LLMSplitScrubberArgs
+ | LLMCreateTrackArgs
+ | LLMMoveScrubbersByOffsetArgs
+ | LLMUpdateTextContentArgs
+ | LLMUpdateTextStyleArgs
| None
) = None
assistant_message: str | None = None
diff --git a/backend/api/routes.py b/backend/api/routes.py
index f8b770f8..110cbe39 100644
--- a/backend/api/routes.py
+++ b/backend/api/routes.py
@@ -1,97 +1,126 @@
import json
+import logging
+from typing import Any
+from uuid import UUID
-from fastapi import APIRouter, Depends, HTTPException, status
+from fastapi import APIRouter, Depends, HTTPException, Path, Query, status
-from api.schema import CreateProjectRequest
+from api.schema import (
+ CreateProjectRequest,
+ ProjectCreateResponse,
+ ProjectListResponse,
+ ProjectMeta,
+ ProjectMutationResponse,
+ ProjectStateResponse,
+ RenameProjectRequest,
+ StorageResponse,
+ TimelinePayload,
+)
from auth.routes import get_current_user
-from auth.schema import KimuJWT
+from auth.schema import SessionUser
from db import get_db_pool
-router = APIRouter(prefix="/api", tags=["api"])
+logger = logging.getLogger(__name__)
+router = APIRouter(tags=["api"])
-@router.get("/projects")
-async def list_projects(user: KimuJWT = Depends(get_current_user)) -> dict:
- """
- Return all projects for the authenticated user.
- """
+# 2 GB per user (per-tier limits live in user_plans table once introduced).
+_DEFAULT_STORAGE_LIMIT_BYTES = 2 * 1024 * 1024 * 1024
+
+
+def _row_to_meta(row: Any) -> ProjectMeta:
+ return ProjectMeta(
+ id=str(row["id"]),
+ user_id=str(row["user_id"]),
+ name=row["name"],
+ created_at=row["created_at"],
+ updated_at=row["updated_at"],
+ )
+
+
+@router.get("/projects", response_model=ProjectListResponse)
+async def list_projects(
+ user: SessionUser = Depends(get_current_user),
+ limit: int = Query(default=20, ge=1, le=100),
+ offset: int = Query(default=0, ge=0),
+) -> ProjectListResponse:
pool = await get_db_pool()
async with pool.acquire() as conn:
rows = await conn.fetch(
"""
- SELECT id, name, created_at
+ SELECT id, user_id, name, created_at, updated_at
FROM projects
WHERE user_id = $1
ORDER BY created_at DESC
+ LIMIT $2 OFFSET $3
""",
user.user_id,
+ limit,
+ offset,
+ )
+ total = await conn.fetchval(
+ "SELECT COUNT(*) FROM projects WHERE user_id = $1",
+ user.user_id,
)
- projects = [
- {
- "id": str(row["id"]),
- "name": str(row["name"]),
- "created_at": row["created_at"].isoformat(),
- }
- for row in rows
- ]
-
- return {"projects": projects}
+ return ProjectListResponse(
+ projects=[_row_to_meta(row) for row in rows],
+ total=int(total or 0),
+ limit=limit,
+ offset=offset,
+ )
-@router.post("/create-project", status_code=status.HTTP_201_CREATED)
+@router.post(
+ "/projects",
+ status_code=status.HTTP_201_CREATED,
+ response_model=ProjectCreateResponse,
+)
async def create_project(
body: CreateProjectRequest,
- user: KimuJWT = Depends(get_current_user),
-) -> dict:
- """
- Create a new project for the authenticated user.
- """
+ user: SessionUser = Depends(get_current_user),
+) -> ProjectCreateResponse:
pool = await get_db_pool()
async with pool.acquire() as conn:
row = await conn.fetchrow(
"""
INSERT INTO projects (user_id, name)
VALUES ($1, $2)
- RETURNING id, name, created_at
+ RETURNING id, user_id, name, created_at, updated_at
""",
user.user_id,
body.name,
)
if row is None:
+ logger.error("INSERT INTO projects returned no row for user %s", user.user_id)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to create project",
)
- return {
- "project": {
- "id": str(row["id"]),
- "name": str(row["name"]),
- "created_at": row["created_at"].isoformat(),
- }
- }
+ logger.info("Project created: %s by user %s", str(row["id"]), user.user_id)
+ return ProjectCreateResponse(project=_row_to_meta(row))
-@router.put("/projects/{project_id}")
+@router.put("/projects/{project_id}", response_model=ProjectMutationResponse)
async def save_project(
- project_id: str, timeline: dict, user: KimuJWT = Depends(get_current_user)
-) -> dict:
- """
- Save the project timeline to the database.
- """
+ timeline: TimelinePayload,
+ project_id: UUID = Path(...),
+ user: SessionUser = Depends(get_current_user),
+) -> ProjectMutationResponse:
pool = await get_db_pool()
async with pool.acquire() as conn:
row = await conn.fetchrow(
"""
UPDATE projects
- SET timeline_state = $1::jsonb
+ SET timeline_state = $1::jsonb,
+ updated_at = now()
WHERE id = $2 AND user_id = $3
RETURNING id
""",
- json.dumps(timeline),
- project_id,
+ json.dumps(timeline.model_dump(mode="json")),
+ str(project_id),
user.user_id,
)
@@ -101,4 +130,112 @@ async def save_project(
detail="Project not found",
)
- return {"ok": True, "project_id": str(row["id"])}
+ return ProjectMutationResponse(ok=True, project_id=str(row["id"]))
+
+
+@router.get("/projects/{project_id}", response_model=ProjectStateResponse)
+async def get_project(
+ project_id: UUID = Path(...),
+ user: SessionUser = Depends(get_current_user),
+) -> ProjectStateResponse:
+ pool = await get_db_pool()
+ async with pool.acquire() as conn:
+ row = await conn.fetchrow(
+ """
+ SELECT id, user_id, name, created_at, updated_at, timeline_state
+ FROM projects
+ WHERE id = $1 AND user_id = $2
+ """,
+ str(project_id),
+ user.user_id,
+ )
+
+ if row is None:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail="Project not found",
+ )
+
+ timeline_raw = row["timeline_state"]
+ if isinstance(timeline_raw, str):
+ try:
+ timeline_raw = json.loads(timeline_raw)
+ except json.JSONDecodeError:
+ timeline_raw = None
+
+ return ProjectStateResponse(
+ project=_row_to_meta(row),
+ timeline=timeline_raw if isinstance(timeline_raw, dict) else {"tracks": []},
+ textBinItems=[],
+ )
+
+
+@router.patch("/projects/{project_id}", response_model=ProjectMutationResponse)
+async def rename_project(
+ body: RenameProjectRequest,
+ project_id: UUID = Path(...),
+ user: SessionUser = Depends(get_current_user),
+) -> ProjectMutationResponse:
+ pool = await get_db_pool()
+ async with pool.acquire() as conn:
+ row = await conn.fetchrow(
+ """
+ UPDATE projects
+ SET name = $1
+ WHERE id = $2 AND user_id = $3
+ RETURNING id
+ """,
+ body.name,
+ str(project_id),
+ user.user_id,
+ )
+
+ if row is None:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail="Project not found",
+ )
+
+ return ProjectMutationResponse(ok=True, project_id=str(row["id"]))
+
+
+@router.get("/storage", response_model=StorageResponse)
+async def get_storage(user: SessionUser = Depends(get_current_user)) -> StorageResponse:
+ pool = await get_db_pool()
+ async with pool.acquire() as conn:
+ used_bytes = await conn.fetchval(
+ """
+ SELECT COALESCE(SUM(file_size), 0)
+ FROM assets
+ WHERE user_id = $1
+ AND deleted_at IS NULL
+ AND status = 'ready'
+ """,
+ user.user_id,
+ )
+ return StorageResponse(
+ usedBytes=int(used_bytes or 0),
+ limitBytes=_DEFAULT_STORAGE_LIMIT_BYTES,
+ )
+
+
+@router.delete("/projects/{project_id}", status_code=status.HTTP_204_NO_CONTENT)
+async def delete_project(
+ project_id: UUID = Path(...),
+ user: SessionUser = Depends(get_current_user),
+) -> None:
+ pool = await get_db_pool()
+ async with pool.acquire() as conn:
+ result = await conn.execute(
+ "DELETE FROM projects WHERE id = $1 AND user_id = $2",
+ str(project_id),
+ user.user_id,
+ )
+
+ if result == "DELETE 0":
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail="Project not found",
+ )
+
+ logger.info("Project deleted: %s by user %s", project_id, user.user_id)
diff --git a/backend/api/schema.py b/backend/api/schema.py
index e5242605..0cf834cc 100644
--- a/backend/api/schema.py
+++ b/backend/api/schema.py
@@ -1,5 +1,72 @@
-from pydantic import BaseModel, Field
+from datetime import datetime
+from typing import Any
+
+from pydantic import BaseModel, ConfigDict, Field
class CreateProjectRequest(BaseModel):
- name: str = Field(description="The name of the project")
+ name: str = Field(
+ min_length=1, max_length=255, description="The name of the project"
+ )
+
+
+class RenameProjectRequest(BaseModel):
+ name: str = Field(
+ min_length=1, max_length=255, description="The new name for the project"
+ )
+
+
+class TimelineTrackPayload(BaseModel):
+ # extra="allow" is intentional: the full timeline (including transitions and
+ # scrubber fields) is passed through and stored verbatim as JSONB.
+ model_config = ConfigDict(extra="allow")
+
+ scrubbers: list[dict] = Field(
+ default_factory=list, description="Track scrubbers list"
+ )
+
+
+class TimelinePayload(BaseModel):
+ model_config = ConfigDict(extra="allow")
+
+ tracks: list[TimelineTrackPayload] = Field(
+ ..., description="Timeline tracks payload"
+ )
+
+
+# ─── Response models ─────────────────────────────────────────────────────────
+
+
+class ProjectMeta(BaseModel):
+ id: str
+ user_id: str
+ name: str
+ created_at: datetime
+ updated_at: datetime
+
+
+class ProjectListResponse(BaseModel):
+ projects: list[ProjectMeta]
+ total: int
+ limit: int
+ offset: int
+
+
+class ProjectCreateResponse(BaseModel):
+ project: ProjectMeta
+
+
+class ProjectStateResponse(BaseModel):
+ project: ProjectMeta
+ timeline: dict[str, Any]
+ textBinItems: list[dict[str, Any]]
+
+
+class ProjectMutationResponse(BaseModel):
+ ok: bool
+ project_id: str
+
+
+class StorageResponse(BaseModel):
+ usedBytes: int
+ limitBytes: int
diff --git a/backend/auth/routes.py b/backend/auth/routes.py
index 0fea98ed..657cd900 100644
--- a/backend/auth/routes.py
+++ b/backend/auth/routes.py
@@ -1,175 +1,81 @@
-from fastapi import APIRouter, Cookie, Depends, HTTPException, status
-from fastapi.responses import JSONResponse
-
-from auth.schema import KimuJWT, KimuPayload, SignUpGoogleRequest
-from auth.service import (
- COOKIE_MAX_AGE,
- COOKIE_NAME,
- generate_kimu_jwt,
- verify_google_id_token,
- verify_kimu_jwt,
-)
+import logging
+from urllib.parse import unquote
+
+from fastapi import APIRouter, Depends, HTTPException, Request, status
+
+from auth.schema import SessionUser
from db import get_db_pool
-from utils import require_env
-router = APIRouter(prefix="/auth", tags=["auth"])
+logger = logging.getLogger(__name__)
-GOOGLE_CLIENT_ID: str = require_env("VITE_GOOGLE_CLIENT_ID")
-JWT_SECRET: str = require_env("JWT_SECRET")
+# HTTPS production uses the __Secure- prefix; dev may use the plain name.
+_BETTER_AUTH_COOKIE_NAMES = (
+ "__Secure-better-auth.session_token",
+ "better-auth.session_token",
+)
-async def get_current_user(
- kimu_session: str = Cookie(alias=COOKIE_NAME),
-) -> KimuJWT:
+def _extract_session_token_from_cookies(request: Request) -> str | None:
"""
- FastAPI dependency. Reads the session JWT from the HttpOnly cookie.
+ Better Auth stores a signed cookie value as ".".
+ Extract the raw token used in the session table.
"""
- try:
- return verify_kimu_jwt(kimu_session, JWT_SECRET)
- except Exception as exc:
- raise HTTPException(
- status_code=status.HTTP_401_UNAUTHORIZED,
- detail=str(exc),
- ) from exc
+ raw_cookie_value: str | None = None
+ for cookie_name in _BETTER_AUTH_COOKIE_NAMES:
+ raw_cookie_value = request.cookies.get(cookie_name)
+ if raw_cookie_value:
+ break
+ if not raw_cookie_value:
+ return None
+
+ decoded_cookie = unquote(raw_cookie_value)
+ token = decoded_cookie.split(".", 1)[0]
+ return token or None
+
+
+router = APIRouter(prefix="/auth", tags=["auth"])
-@router.post("/google")
-async def google_sign_in(body: SignUpGoogleRequest) -> JSONResponse:
+async def get_current_user(
+ request: Request,
+) -> SessionUser:
"""
- Verify the Google ID token, upsert the user, return user info and
- set an HttpOnly session cookie with the Kimu JWT.
+ FastAPI dependency. Reads the BetterAuth session token from the HttpOnly
+ cookie and validates it against the session/user tables in Postgres.
"""
- # 1. Verify the Google credential
- try:
- google_user = verify_google_id_token(body.credential, GOOGLE_CLIENT_ID)
- except ValueError as exc:
+ session_token = _extract_session_token_from_cookies(request)
+ if not session_token:
raise HTTPException(
- status_code=status.HTTP_401_UNAUTHORIZED,
- detail=f"Google token verification failed: {exc}",
- ) from exc
+ status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authenticated"
+ )
- # 2. Upsert user + identity in Postgres
pool = await get_db_pool()
- async with pool.acquire() as conn, conn.transaction():
+ async with pool.acquire() as conn:
row = await conn.fetchrow(
"""
- SELECT u.id, u.email, u.name
- FROM user_identities ui
- JOIN users u ON u.id = ui.user_id
- WHERE ui.provider = $1 AND ui.provider_sub = $2
+ SELECT u.id, u.name, u.email, u.image
+ FROM session s
+ JOIN "user" u ON u.id = s."userId"
+ WHERE s.token = $1 AND s."expiresAt" > now()
""",
- "google",
- google_user.sub,
+ session_token,
)
- if row is None:
- # Create or reuse the user row by email, then link Google identity.
- user_row = await conn.fetchrow(
- """
- INSERT INTO users (email, name)
- VALUES ($1, $2)
- ON CONFLICT (email)
- DO NOTHING
- RETURNING id, email, name
- """,
- google_user.email,
- google_user.name,
- )
-
- if user_row is None:
- user_row = await conn.fetchrow(
- """
- SELECT id, email, name
- FROM users
- WHERE email = $1
- """,
- google_user.email,
- )
- if user_row is None:
- raise HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail="Failed to create or fetch user",
- )
-
- await conn.execute(
- """
- INSERT INTO user_identities (user_id, provider, provider_sub)
- VALUES ($1, $2, $3)
- ON CONFLICT (provider, provider_sub) DO NOTHING
- """,
- user_row["id"],
- "google",
- google_user.sub,
- )
-
- row = await conn.fetchrow(
- """
- SELECT u.id, u.email, u.name
- FROM user_identities ui
- JOIN users u ON u.id = ui.user_id
- WHERE ui.provider = $1 AND ui.provider_sub = $2
- """,
- "google",
- google_user.sub,
- )
-
- if row is None:
- raise HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail="Failed to create or fetch user identity",
- )
-
- user_id = str(row["id"])
- user_email = str(row["email"])
- user_name = str(row["name"])
-
- # 3. Generate Kimu JWT
- payload = KimuPayload(
- user_id=user_id,
- email=user_email,
- name=user_name,
- avatar_url=google_user.picture,
- )
- token = generate_kimu_jwt(payload, JWT_SECRET)
-
- # 4. Build response with HttpOnly cookie
- body_data = KimuPayload(
- user_id=user_id,
- email=user_email,
- name=user_name,
- avatar_url=google_user.picture,
- )
- response = JSONResponse(content=body_data.model_dump())
- response.set_cookie(
- key=COOKIE_NAME,
- value=token,
- max_age=COOKIE_MAX_AGE,
- httponly=True,
- secure=True,
- samesite="lax",
- path="/",
- )
- return response
-
+ if row is None:
+ logger.warning("Invalid or expired session token attempted")
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Invalid or expired session",
+ )
-@router.get("/me", response_model=KimuPayload)
-async def get_me(user: KimuJWT = Depends(get_current_user)) -> KimuPayload:
- """
- Return the current user's profile from the JWT.
- """
- return KimuPayload(
- user_id=user.user_id,
- email=user.email,
- name=user.name,
- avatar_url=user.avatar_url,
+ return SessionUser(
+ user_id=str(row["id"]),
+ email=str(row["email"]),
+ name=str(row["name"]),
+ image=str(row["image"]) if row["image"] else None,
)
-@router.post("/logout")
-async def logout() -> JSONResponse:
- """
- Log out the current user by clearing the HttpOnly session cookie.
- """
- response = JSONResponse(content={})
- response.delete_cookie(key=COOKIE_NAME)
- return response
+@router.get("/me", response_model=SessionUser)
+async def get_me(user: SessionUser = Depends(get_current_user)) -> SessionUser:
+ return user
diff --git a/backend/auth/schema.py b/backend/auth/schema.py
index 6218b9db..5d719038 100644
--- a/backend/auth/schema.py
+++ b/backend/auth/schema.py
@@ -1,29 +1,10 @@
from pydantic import BaseModel
-class SignUpGoogleRequest(BaseModel):
- credential: str # Google ID token from the client
+class SessionUser(BaseModel):
+ """User resolved from a valid BetterAuth session."""
-
-class GoogleJWT(BaseModel):
- """Subset of claims we need from a verified Google ID token."""
-
- sub: str # stable unique Google user ID
- email: str
- name: str
- picture: str # URL of the user's profile picture, not stored in the database
-
-
-class KimuPayload(BaseModel):
- """Claims embedded in Kimu's own application JWT. This is the payload that is signed by the server and sent to the client."""
-
- user_id: str # UUID as string
+ user_id: str
email: str
name: str
- avatar_url: str
-
-
-class KimuJWT(KimuPayload):
- """Decoded Kimu JWT (includes the expiration claim)."""
-
- exp: int
+ image: str | None = None
diff --git a/backend/auth/service.py b/backend/auth/service.py
deleted file mode 100644
index df9794ce..00000000
--- a/backend/auth/service.py
+++ /dev/null
@@ -1,79 +0,0 @@
-import datetime
-
-import jwt
-from google.auth.transport import requests as google_requests
-from google.oauth2 import id_token
-
-from auth.schema import GoogleJWT, KimuJWT, KimuPayload
-
-COOKIE_NAME = "kimu_session"
-COOKIE_MAX_AGE = 30 * 24 * 60 * 60 # 30 days in seconds
-
-
-def verify_google_id_token(token: str, client_id: str) -> GoogleJWT:
- """Verify a Google ID token and return the decoded claims.
-
- Uses google-auth which handles JWKS fetching, key rotation, signature
- verification, and iss/aud/exp validation internally.
-
- Raises ValueError on any verification failure.
- """
- request = google_requests.Request()
-
- id_info: dict[str, object] = id_token.verify_oauth2_token(
- token, request, audience=client_id
- )
-
- issuer = id_info.get("iss")
- if issuer not in ("accounts.google.com", "https://accounts.google.com"):
- raise ValueError(f"Invalid issuer: {issuer}")
-
- # i still dont know how email can NOT be verified. but it is a field and guess we'll follow the spec.
- if not id_info.get("email_verified"):
- raise ValueError("Email address is not verified by Google")
-
- return GoogleJWT(
- sub=str(id_info["sub"]),
- email=str(id_info["email"]),
- name=str(id_info["name"]),
- picture=str(id_info["picture"]),
- )
-
-
-def generate_kimu_jwt(payload: KimuPayload, secret_key: str) -> str:
- """Generate a signed HS256 JWT for Kimu sessions."""
- expiration = (
- datetime.datetime.now(datetime.UTC) + datetime.timedelta(days=30)
- ) # TODO: this is a very basic mvp ship. we need to migrate to a better solution with token rotation.
-
- token: str = jwt.encode(
- {
- "user_id": payload.user_id,
- "email": payload.email,
- "name": payload.name,
- "avatar_url": payload.avatar_url,
- "exp": expiration,
- },
- secret_key,
- algorithm="HS256",
- )
- return token
-
-
-def verify_kimu_jwt(token: str, secret_key: str) -> KimuJWT:
- """Decode and verify a Kimu application JWT.
-
- Raises jwt.ExpiredSignatureError if the token is expired.
- Raises jwt.InvalidTokenError for any other verification failure.
- """
- decoded: dict[str, object] = jwt.decode(token, secret_key, algorithms=["HS256"])
- raw_exp = decoded["exp"]
- if not isinstance(raw_exp, int):
- raise jwt.InvalidTokenError("Missing or invalid exp claim")
- return KimuJWT(
- user_id=str(decoded["user_id"]),
- email=str(decoded["email"]),
- name=str(decoded["name"]),
- avatar_url=str(decoded["avatar_url"]),
- exp=raw_exp,
- )
diff --git a/backend/db.py b/backend/db.py
index 9d179645..d38ec7f3 100644
--- a/backend/db.py
+++ b/backend/db.py
@@ -1,19 +1,41 @@
-from typing import Optional
+import logging
+import os
import asyncpg # type: ignore[import-untyped]
from utils import require_env
+logger = logging.getLogger(__name__)
+
DATABASE_URL: str = require_env("DATABASE_URL")
_pool: asyncpg.Pool | None = None
async def get_db_pool() -> asyncpg.Pool:
- """
- Return the shared asyncpg connection pool, creating it on first call.
- """
+ """Return the shared asyncpg connection pool, creating it on first call."""
global _pool
if _pool is None:
- _pool = await asyncpg.create_pool(DATABASE_URL)
+ ssl = "require" if os.getenv("DATABASE_SSL") == "true" else None
+ try:
+ _pool = await asyncpg.create_pool(
+ DATABASE_URL,
+ ssl=ssl,
+ min_size=2,
+ max_size=20,
+ command_timeout=30,
+ )
+ logger.info("Database pool created")
+ except Exception:
+ logger.exception("Failed to create database pool")
+ raise
return _pool
+
+
+async def close_db_pool() -> None:
+ """Gracefully close the connection pool on shutdown."""
+ global _pool
+ if _pool is not None:
+ await _pool.close()
+ _pool = None
+ logger.info("Database pool closed")
diff --git a/backend/main.py b/backend/main.py
index 692eb33a..172398b6 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -1,3 +1,7 @@
+import logging
+import os
+from collections.abc import AsyncIterator
+from contextlib import asynccontextmanager
from pathlib import Path
from dotenv import load_dotenv
@@ -10,20 +14,36 @@
from ai.routes import router as ai_router # noqa: E402
from api.routes import router as api_router # noqa: E402
from auth.routes import router as auth_router # noqa: E402
+from db import close_db_pool # noqa: E402
-app = FastAPI()
+logging.basicConfig(
+ level=logging.INFO,
+ format="%(asctime)s %(levelname)s %(name)s: %(message)s",
+)
+logger = logging.getLogger(__name__)
+
+
+@asynccontextmanager
+async def lifespan(app: FastAPI) -> AsyncIterator[None]:
+ logger.info("Starting up")
+ yield
+ logger.info("Shutting down — closing DB pool")
+ await close_db_pool()
+
+
+app = FastAPI(lifespan=lifespan, redirect_slashes=False)
_ALLOWED_ORIGINS = [
"https://trykimu.com",
- "http://localhost:8080", # this is a lil finnicky but it works for now. we will move to an env based permanent solution later.
+ "http://localhost:5173", # Vite dev server
]
app.add_middleware(
CORSMiddleware,
allow_origins=_ALLOWED_ORIGINS,
allow_credentials=True,
- allow_methods=["GET", "POST", "PATCH", "DELETE", "OPTIONS"],
- allow_headers=["Content-Type"],
+ allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
+ allow_headers=["Content-Type", "Authorization"],
)
@@ -39,4 +59,9 @@ async def beep() -> dict:
if __name__ == "__main__":
import uvicorn
- uvicorn.run("main:app", host="0.0.0.0", port=3000, reload=True)
+ uvicorn.run(
+ "main:app",
+ host=os.getenv("HOST", "0.0.0.0"),
+ port=int(os.getenv("PORT", "3000")),
+ reload=os.getenv("NODE_ENV") != "production",
+ )
diff --git a/backend/pyproject.toml b/backend/pyproject.toml
index 822ec458..b4e859e9 100644
--- a/backend/pyproject.toml
+++ b/backend/pyproject.toml
@@ -12,7 +12,7 @@ dependencies = [
"asyncpg>=0.31.0",
"fastapi[standard]>=0.115.13",
"google-genai>=1.22.0",
- "pyjwt>=2.11.0",
+ "python-dotenv>=1.0.0",
"python-multipart>=0.0.22",
]
diff --git a/backend/uv.lock b/backend/uv.lock
index 7e23526a..2df75287 100644
--- a/backend/uv.lock
+++ b/backend/uv.lock
@@ -86,7 +86,7 @@ dependencies = [
{ name = "asyncpg" },
{ name = "fastapi", extra = ["standard"] },
{ name = "google-genai" },
- { name = "pyjwt" },
+ { name = "python-dotenv" },
{ name = "python-multipart" },
]
@@ -102,7 +102,7 @@ requires-dist = [
{ name = "asyncpg", specifier = ">=0.31.0" },
{ name = "fastapi", extras = ["standard"], specifier = ">=0.115.13" },
{ name = "google-genai", specifier = ">=1.22.0" },
- { name = "pyjwt", specifier = ">=2.11.0" },
+ { name = "python-dotenv", specifier = ">=1.0.0" },
{ name = "python-multipart", specifier = ">=0.0.22" },
]
@@ -1025,15 +1025,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
]
-[[package]]
-name = "pyjwt"
-version = "2.11.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/5c/5a/b46fa56bf322901eee5b0454a34343cdbdae202cd421775a8ee4e42fd519/pyjwt-2.11.0.tar.gz", hash = "sha256:35f95c1f0fbe5d5ba6e43f00271c275f7a1a4db1dab27bf708073b75318ea623", size = 98019, upload-time = "2026-01-30T19:59:55.694Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/6f/01/c26ce75ba460d5cd503da9e13b21a33804d38c2165dec7b716d06b13010c/pyjwt-2.11.0-py3-none-any.whl", hash = "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469", size = 28224, upload-time = "2026-01-30T19:59:54.539Z" },
-]
-
[[package]]
name = "python-discovery"
version = "1.1.0"
diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml
index 705fee5a..a2837b5a 100644
--- a/docker-compose.dev.yml
+++ b/docker-compose.dev.yml
@@ -1,4 +1,19 @@
services:
+ redis:
+ image: redis:7-alpine
+ container_name: videoeditor-redis-dev
+ restart: unless-stopped
+ command: redis-server --appendonly yes
+ ports:
+ - "6379:6379"
+ volumes:
+ - redis_data_dev:/data
+ healthcheck:
+ test: ["CMD", "redis-cli", "ping"]
+ interval: 5s
+ timeout: 3s
+ retries: 5
+
postgres:
image: postgres:18
container_name: videoeditor-postgres-dev
@@ -6,14 +21,20 @@ services:
environment:
POSTGRES_DB: videoeditor
POSTGRES_USER: videoeditor
- POSTGRES_PASSWORD: videoeditor
+ POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-videoeditor}
ports:
- "5432:5432"
-
- nginx:
- image: nginx:latest
- container_name: videoeditor-nginx-dev
- ports:
- - "8080:80"
volumes:
- - ./nginx.dev.conf:/etc/nginx/nginx.conf:ro
+ # Persist data across `docker compose down` so dev DBs survive restarts.
+ - postgres_data_dev:/var/lib/postgresql/data
+ # Auto-run migrations on first init only (postgres entrypoint convention).
+ - ./migrations:/docker-entrypoint-initdb.d:ro
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U videoeditor -d videoeditor"]
+ interval: 5s
+ timeout: 3s
+ retries: 5
+
+volumes:
+ postgres_data_dev:
+ redis_data_dev:
diff --git a/docker-compose.yml b/docker-compose.yml
index 84cf6840..fd1d4247 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,4 +1,17 @@
services:
+ redis:
+ image: redis:7-alpine
+ container_name: videoeditor-redis
+ restart: unless-stopped
+ command: redis-server --appendonly yes
+ volumes:
+ - redis_data:/data
+ healthcheck:
+ test: ["CMD", "redis-cli", "ping"]
+ interval: 5s
+ timeout: 3s
+ retries: 5
+
nginx:
image: nginx:alpine
container_name: videoeditor-nginx
@@ -9,31 +22,48 @@ services:
- ./nginx.conf:/etc/nginx/nginx.conf
- /etc/letsencrypt:/etc/letsencrypt:ro # Mount certs read-only
depends_on:
- - frontend
- - backend
+ redis:
+ condition: service_healthy
+ frontend:
+ condition: service_healthy
+ backend:
+ condition: service_healthy
+ fastapi:
+ condition: service_healthy
+ restart: unless-stopped
frontend:
build:
context: .
dockerfile: Dockerfile.frontend
- args:
- VITE_SUPABASE_URL: ${VITE_SUPABASE_URL}
- VITE_SUPABASE_ANON_KEY: ${VITE_SUPABASE_ANON_KEY}
container_name: videoeditor-frontend
env_file:
- .env
environment:
- # Ensure server-side code can construct proper callback URLs
- AUTH_BASE_URL: https://trykimu.com
- AUTH_TRUSTED_ORIGINS: https://trykimu.com,https://www.trykimu.com
- AUTH_COOKIE_DOMAIN: trykimu.com
+ BETTER_AUTH_URL: https://trykimu.com
+ BACKEND_INTERNAL_URL: http://fastapi:3000
NODE_ENV: production
HOST: 0.0.0.0
PORT: 3000
+ REDIS_URL: redis://redis:6379/0
# ports:
# - "3000:3000"
depends_on:
- - backend
+ redis:
+ condition: service_healthy
+ backend:
+ condition: service_started
+ healthcheck:
+ test:
+ [
+ "CMD-SHELL",
+ 'node -e "require(''http'').get(''http://127.0.0.1:3000/'', res => process.exit(res.statusCode < 500 ? 0 : 1)).on(''error'', () => process.exit(1))"',
+ ]
+ interval: 15s
+ timeout: 5s
+ retries: 5
+ start_period: 30s
+ restart: unless-stopped
backend:
build:
@@ -43,22 +73,53 @@ services:
env_file:
- .env
environment:
- AUTH_BASE_URL: https://trykimu.com
- AUTH_TRUSTED_ORIGINS: https://trykimu.com,https://www.trykimu.com
- AUTH_COOKIE_DOMAIN: trykimu.com
+ BETTER_AUTH_URL: https://trykimu.com
NODE_ENV: production
PORT: 8000
+ REDIS_URL: redis://redis:6379/0
# ports:
# - "8000:8000"
- volumes:
- - ./out:/app/out
- # Memory configuration for video rendering
- mem_limit: 2g
- memswap_limit: 2g
- shm_size: 1g
+ # Remotion + FFmpeg need headroom for 1080p/4K stitch (see remotion.dev/docs/troubleshooting/sigkill)
+ mem_limit: 4g
+ memswap_limit: 4g
+ shm_size: 2g
+ depends_on:
+ redis:
+ condition: service_healthy
+ healthcheck:
+ test:
+ [
+ "CMD-SHELL",
+ 'node -e "require(''http'').get(''http://127.0.0.1:8000/health'', res => process.exit(res.statusCode === 200 ? 0 : 1)).on(''error'', () => process.exit(1))"',
+ ]
+ interval: 15s
+ timeout: 5s
+ retries: 5
+ start_period: 30s
+ restart: unless-stopped
fastapi:
build:
context: ./backend
dockerfile: Dockerfile
container_name: videoeditor-fastapi
+ env_file:
+ - .env
+ environment:
+ NODE_ENV: production
+ HOST: 0.0.0.0
+ PORT: 3000
+ REDIS_URL: redis://redis:6379/0
+ depends_on:
+ redis:
+ condition: service_healthy
+ healthcheck:
+ test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:3000/beep >/dev/null"]
+ interval: 15s
+ timeout: 5s
+ retries: 5
+ start_period: 30s
+ restart: unless-stopped
+
+volumes:
+ redis_data:
diff --git a/eslint.config.js b/eslint.config.js
index 68241cef..a9249e90 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -6,6 +6,20 @@ import reactHooksPlugin from "eslint-plugin-react-hooks";
import remotionPlugin from "@remotion/eslint-plugin";
export default [
+ {
+ ignores: [
+ ".react-router/**",
+ "build/**",
+ "dist/**",
+ "out/**",
+ "public/**",
+ "node_modules/**",
+ "coverage/**",
+ "backend/.venv/**",
+ "backend/**/__pycache__/**",
+ "**/*.min.js",
+ ],
+ },
eslint.configs.recommended,
{
files: ["**/*.{ts,tsx}"],
diff --git a/migrations/000_betterauth.sql b/migrations/000_betterauth.sql
new file mode 100644
index 00000000..fe08220b
--- /dev/null
+++ b/migrations/000_betterauth.sql
@@ -0,0 +1,120 @@
+CREATE EXTENSION IF NOT EXISTS pgcrypto;
+
+-- BetterAuth: user table
+CREATE TABLE IF NOT EXISTS "user" (
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
+ name TEXT NOT NULL,
+ email TEXT NOT NULL UNIQUE,
+ "emailVerified" BOOLEAN NOT NULL DEFAULT FALSE,
+ image TEXT,
+ "createdAt" TIMESTAMPTZ NOT NULL DEFAULT now(),
+ "updatedAt" TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+
+-- BetterAuth: session table
+CREATE TABLE IF NOT EXISTS session (
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
+ "expiresAt" TIMESTAMPTZ NOT NULL,
+ token TEXT NOT NULL UNIQUE,
+ "createdAt" TIMESTAMPTZ NOT NULL DEFAULT now(),
+ "updatedAt" TIMESTAMPTZ NOT NULL DEFAULT now(),
+ "ipAddress" TEXT,
+ "userAgent" TEXT,
+ "userId" TEXT NOT NULL REFERENCES "user"(id) ON DELETE CASCADE
+);
+
+-- BetterAuth: account table
+CREATE TABLE IF NOT EXISTS account (
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
+ "accountId" TEXT NOT NULL,
+ "providerId" TEXT NOT NULL,
+ "userId" TEXT NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
+ "accessToken" TEXT,
+ "refreshToken" TEXT,
+ "idToken" TEXT,
+ "accessTokenExpiresAt" TIMESTAMPTZ,
+ "refreshTokenExpiresAt" TIMESTAMPTZ,
+ scope TEXT,
+ password TEXT,
+ "createdAt" TIMESTAMPTZ NOT NULL DEFAULT now(),
+ "updatedAt" TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+
+-- BetterAuth: verification table
+CREATE TABLE IF NOT EXISTS verification (
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
+ identifier TEXT NOT NULL,
+ value TEXT NOT NULL,
+ "expiresAt" TIMESTAMPTZ NOT NULL,
+ "createdAt" TIMESTAMPTZ NOT NULL DEFAULT now(),
+ "updatedAt" TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+
+-- Guard against adapters that pass null/empty IDs.
+CREATE OR REPLACE FUNCTION set_user_id() RETURNS TRIGGER AS $$
+BEGIN
+ IF NEW.id IS NULL OR NEW.id = '' THEN
+ NEW.id = gen_random_uuid()::text;
+ END IF;
+ RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION set_verification_id() RETURNS TRIGGER AS $$
+BEGIN
+ IF NEW.id IS NULL OR NEW.id = '' THEN
+ NEW.id = gen_random_uuid()::text;
+ END IF;
+ RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+
+-- Keep BetterAuth updatedAt current.
+CREATE OR REPLACE FUNCTION set_updated_at() RETURNS TRIGGER AS $$
+BEGIN
+ NEW."updatedAt" = now();
+ RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+
+DROP TRIGGER IF EXISTS trg_user_updated_at ON "user";
+CREATE TRIGGER trg_user_updated_at
+ BEFORE UPDATE ON "user"
+ FOR EACH ROW
+ EXECUTE FUNCTION set_updated_at();
+
+DROP TRIGGER IF EXISTS trg_session_updated_at ON session;
+CREATE TRIGGER trg_session_updated_at
+ BEFORE UPDATE ON session
+ FOR EACH ROW
+ EXECUTE FUNCTION set_updated_at();
+
+DROP TRIGGER IF EXISTS trg_account_updated_at ON account;
+CREATE TRIGGER trg_account_updated_at
+ BEFORE UPDATE ON account
+ FOR EACH ROW
+ EXECUTE FUNCTION set_updated_at();
+
+DROP TRIGGER IF EXISTS trg_verification_updated_at ON verification;
+CREATE TRIGGER trg_verification_updated_at
+ BEFORE UPDATE ON verification
+ FOR EACH ROW
+ EXECUTE FUNCTION set_updated_at();
+
+DROP TRIGGER IF EXISTS trg_user_set_id ON "user";
+CREATE TRIGGER trg_user_set_id
+ BEFORE INSERT ON "user"
+ FOR EACH ROW
+ EXECUTE FUNCTION set_user_id();
+
+DROP TRIGGER IF EXISTS trg_verification_set_id ON verification;
+CREATE TRIGGER trg_verification_set_id
+ BEFORE INSERT ON verification
+ FOR EACH ROW
+ EXECUTE FUNCTION set_verification_id();
+
+CREATE INDEX IF NOT EXISTS idx_session_user_id ON session("userId");
+CREATE INDEX IF NOT EXISTS idx_session_token ON session(token);
+CREATE INDEX IF NOT EXISTS idx_account_user_id ON account("userId");
+CREATE INDEX IF NOT EXISTS idx_account_provider ON account("providerId", "accountId");
+CREATE INDEX IF NOT EXISTS idx_verification_identifier ON verification(identifier);
diff --git a/migrations/000_init.sql b/migrations/000_init.sql
deleted file mode 100644
index 4a6b4f5e..00000000
--- a/migrations/000_init.sql
+++ /dev/null
@@ -1,39 +0,0 @@
-CREATE TABLE users (
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
- email TEXT NOT NULL UNIQUE,
- name TEXT NOT NULL,
- created_at TIMESTAMPTZ NOT NULL DEFAULT now()
-);
-
-CREATE TABLE projects (
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
- user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
- name TEXT NOT NULL,
- timeline_state JSONB,
- created_at TIMESTAMPTZ NOT NULL DEFAULT now()
-);
-
-
-CREATE TABLE assets (
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
- project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
- storage_key TEXT NOT NULL,
- name TEXT NOT NULL,
- mime_type TEXT NOT NULL,
- size_bytes BIGINT NOT NULL CHECK (size_bytes >= 0),
- metadata JSONB,
- created_at TIMESTAMPTZ NOT NULL DEFAULT now()
-);
-
-CREATE TABLE user_identities (
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
- user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
- provider TEXT NOT NULL, -- google, apple, etc. we only support google for now.
- provider_sub TEXT NOT NULL,
- created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
- UNIQUE (provider, provider_sub) -- ensures each user has only one identity per provider
-);
-
-CREATE INDEX idx_projects_user_id ON projects(user_id, created_at DESC);
-CREATE INDEX idx_assets_project_id ON assets(project_id);
-CREATE INDEX idx_user_identities_user_id ON user_identities(user_id);
diff --git a/migrations/001_projects.sql b/migrations/001_projects.sql
new file mode 100644
index 00000000..506b74b7
--- /dev/null
+++ b/migrations/001_projects.sql
@@ -0,0 +1,29 @@
+-- Projects table with timeline persistence.
+CREATE TABLE IF NOT EXISTS projects (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ user_id TEXT NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
+ name TEXT NOT NULL,
+ timeline_state JSONB NOT NULL DEFAULT '{"tracks":[]}'::jsonb,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+
+-- Keep snake_case updated_at current.
+CREATE OR REPLACE FUNCTION set_updated_at_snake() RETURNS TRIGGER AS $$
+BEGIN
+ NEW.updated_at = now();
+ RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+
+DROP TRIGGER IF EXISTS trg_projects_updated_at ON projects;
+CREATE TRIGGER trg_projects_updated_at
+ BEFORE UPDATE ON projects
+ FOR EACH ROW
+ EXECUTE FUNCTION set_updated_at_snake();
+
+CREATE INDEX IF NOT EXISTS idx_projects_user_created_at
+ ON projects(user_id, created_at DESC);
+
+CREATE INDEX IF NOT EXISTS idx_projects_user_updated_at
+ ON projects(user_id, updated_at DESC);
diff --git a/migrations/002_assets.sql b/migrations/002_assets.sql
new file mode 100644
index 00000000..b530125f
--- /dev/null
+++ b/migrations/002_assets.sql
@@ -0,0 +1,68 @@
+-- Assets feature: Cloudflare R2 storage with content-addressed deduplication.
+--
+-- r2_objects — one row per unique file (SHA-256). Physical storage in R2.
+-- Multiple users can reference the same object.
+-- assets — one row per user+file. References r2_objects via content_hash.
+-- Soft-deleted; R2 object is removed only when no active rows remain.
+
+-- ─── Shared R2 objects (dedup store) ─────────────────────────────────────────
+
+CREATE TABLE IF NOT EXISTS r2_objects (
+ content_hash TEXT PRIMARY KEY, -- SHA-256 hex (64 chars)
+ r2_key TEXT NOT NULL UNIQUE, -- e.g. objects/abc123.mp4
+ file_size BIGINT NOT NULL,
+ mime_type TEXT NOT NULL,
+ status TEXT NOT NULL DEFAULT 'pending'
+ CHECK (status IN ('pending', 'ready')),
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+
+-- ─── Per-user asset records ───────────────────────────────────────────────────
+
+CREATE TABLE IF NOT EXISTS assets (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ user_id TEXT NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
+ project_id UUID REFERENCES projects(id) ON DELETE SET NULL,
+ content_hash TEXT REFERENCES r2_objects(content_hash),
+ r2_key TEXT,
+ filename TEXT,
+ file_size BIGINT,
+ mime_type TEXT,
+ media_type TEXT, -- video | audio | image
+ duration_seconds FLOAT,
+ width INT,
+ height INT,
+ status TEXT NOT NULL DEFAULT 'pending'
+ CHECK (status IN ('pending', 'uploading', 'ready', 'failed')),
+ public_url TEXT, -- legacy URL field (no longer required for auth-protected access)
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ deleted_at TIMESTAMPTZ
+);
+
+DROP TRIGGER IF EXISTS trg_assets_updated_at ON assets;
+CREATE TRIGGER trg_assets_updated_at
+ BEFORE UPDATE ON assets
+ FOR EACH ROW
+ EXECUTE FUNCTION set_updated_at_snake();
+
+DROP TRIGGER IF EXISTS trg_r2_objects_updated_at ON r2_objects;
+CREATE TRIGGER trg_r2_objects_updated_at
+ BEFORE UPDATE ON r2_objects
+ FOR EACH ROW
+ EXECUTE FUNCTION set_updated_at_snake();
+
+-- ─── Indexes ──────────────────────────────────────────────────────────────────
+
+CREATE INDEX IF NOT EXISTS idx_assets_user_created_at
+ ON assets(user_id, created_at DESC);
+
+CREATE INDEX IF NOT EXISTS idx_assets_user_project_created_at
+ ON assets(user_id, project_id, created_at DESC);
+
+CREATE INDEX IF NOT EXISTS idx_assets_status
+ ON assets(user_id, status) WHERE deleted_at IS NULL;
+
+CREATE INDEX IF NOT EXISTS idx_assets_content_hash
+ ON assets(content_hash) WHERE deleted_at IS NULL;
diff --git a/migrations/003_project_renders.sql b/migrations/003_project_renders.sql
new file mode 100644
index 00000000..4925564c
--- /dev/null
+++ b/migrations/003_project_renders.sql
@@ -0,0 +1,24 @@
+-- Export history per project (video + thumbnail in R2, keyed by content fingerprint).
+CREATE TABLE IF NOT EXISTS project_renders (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
+ user_id TEXT NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
+ render_job_id UUID NOT NULL,
+ content_fingerprint TEXT NOT NULL,
+ file_name TEXT NOT NULL,
+ codec TEXT NOT NULL,
+ width INT NOT NULL,
+ height INT NOT NULL,
+ duration_frames INT,
+ crf INT,
+ resolution_preset TEXT,
+ r2_video_key TEXT NOT NULL,
+ r2_thumb_key TEXT,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+
+CREATE INDEX IF NOT EXISTS idx_project_renders_project_created
+ ON project_renders(project_id, created_at DESC);
+
+CREATE INDEX IF NOT EXISTS idx_project_renders_fingerprint
+ ON project_renders(project_id, user_id, content_fingerprint, created_at DESC);
diff --git a/nginx.conf b/nginx.conf
index 55489099..12a25751 100644
--- a/nginx.conf
+++ b/nginx.conf
@@ -3,7 +3,8 @@ events {
}
http {
- client_max_body_size 500M;
+ # Uploads go browser → R2 directly; only render control JSON passes through here
+ client_max_body_size 10M;
upstream frontend {
server frontend:3000;
@@ -35,9 +36,9 @@ http {
# Security Headers
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
- # trykimu.com/ai/api/ai → http://fastapi/ai
- location /ai/api/ {
- rewrite ^/ai/api/(.*)$ /$1 break;
+ # trykimu.com/backend/* → http://fastapi/*
+ location /backend/ {
+ rewrite ^/backend/(.*)$ /$1 break;
proxy_pass http://fastapi;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
@@ -48,9 +49,9 @@ http {
proxy_request_buffering off;
}
- # trykimu.com/render/render → http://backend/render
- location /render/ {
- rewrite ^/render/(.*)$ /$1 break;
+ # trykimu.com/renderer/* → http://backend/*
+ location /renderer/ {
+ rewrite ^/renderer/(.*)$ /$1 break;
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
diff --git a/nginx.dev.conf b/nginx.dev.conf
deleted file mode 100644
index d50858a4..00000000
--- a/nginx.dev.conf
+++ /dev/null
@@ -1,42 +0,0 @@
-events {}
-
-http {
- server {
- client_max_body_size 500M;
- listen 80;
-
- location / {
- proxy_pass http://host.docker.internal:5173;
- proxy_http_version 1.1;
- # http_host includes the port also with the host (localhost:8080) so we need to use it instead of $host
- proxy_set_header Host $http_host;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Proto $scheme;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection "upgrade";
- }
-
- location /api/ {
- proxy_pass http://host.docker.internal:8000/;
- proxy_http_version 1.1;
- proxy_set_header Host $host;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Proto $scheme;
- }
-
- location /ai/api/ {
- rewrite ^/ai/api/(.*)$ /$1 break;
- proxy_pass http://host.docker.internal:3000;
- proxy_http_version 1.1;
- proxy_set_header Host $host;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Proto $scheme;
- proxy_read_timeout 900s;
- proxy_send_timeout 900s;
- proxy_request_buffering off;
- }
- }
-}
diff --git a/package.json b/package.json
index 3dd2726d..5e71207d 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,7 @@
{
"name": "videoeditor",
"private": true,
+ "packageManager": "pnpm@9.15.9",
"type": "module",
"license": "SEE LICENSE IN LICENSE.md",
"scripts": {
@@ -8,11 +9,16 @@
"dev": "react-router dev --host 0.0.0.0",
"start": "react-router-serve ./build/server/index.js",
"typecheck": "react-router typegen && tsc",
- "lint": "eslint . --ext .ts,.tsx",
+ "lint": "eslint \"**/*.{ts,tsx,js,mjs,cjs}\"",
+ "lint:fix": "eslint \"**/*.{ts,tsx,js,mjs,cjs}\" --fix",
"format": "prettier --write .",
- "format:check": "prettier --check ."
+ "format:check": "prettier --check .",
+ "check": "pnpm typecheck && pnpm lint && pnpm format:check"
},
"dependencies": {
+ "@aws-sdk/client-s3": "^3",
+ "@aws-sdk/lib-storage": "^3",
+ "@aws-sdk/s3-request-presigner": "^3",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.15",
@@ -26,9 +32,8 @@
"@radix-ui/react-switch": "^1.2.5",
"@radix-ui/react-tabs": "^1.1.12",
"@radix-ui/react-tooltip": "^1.2.7",
- "@react-oauth/google": "^0.13.4",
- "@react-router/node": "^7.9.5",
- "@react-router/serve": "^7.7.1",
+ "@react-router/node": "^7.14.2",
+ "@react-router/serve": "^7.14.2",
"@remotion/bundler": "4.0.329",
"@remotion/captions": "4.0.331",
"@remotion/cli": "4.0.329",
@@ -39,30 +44,35 @@
"@remotion/transitions": "4.0.329",
"@types/cors": "^2.8.19",
"axios": "^1.13.5",
+ "better-auth": "^1.6.9",
+ "bullmq": "^5.76.5",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cors": "^2.8.5",
"dotenv": "^17.2.1",
"express": "^5.1.0",
"framer-motion": "^12.23.12",
+ "ioredis": "^5.10.1",
"isbot": "^5.1.29",
"lucide-react": "^0.534.0",
"motion": "^12.23.12",
"next-themes": "^0.4.6",
+ "pg": "^8.20.0",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-icons": "^5.3.0",
"react-resizable-panels": "^3.0.4",
- "react-router": "^7.12.0",
+ "react-router": "^7.14.2",
"remotion": "4.0.329",
"sonner": "^2.0.6",
"tailwind-merge": "^3.3.1",
- "vaul": "^1.1.2"
+ "vaul": "^1.1.2",
+ "zod": "^4.4.2"
},
"pnpm": {
"overrides": {
"webpack": ">=5.104.1",
- "zod": "4.0.9",
+ "zod": "^4.4.2",
"vite@>=7.0.0 <=7.0.7": ">=7.0.8",
"js-yaml@>=4.0.0 <4.1.1": ">=4.1.1",
"glob@>=10.2.0 <10.5.0": ">=10.5.0",
@@ -72,11 +82,6 @@
"better-auth@<1.3.26": ">=1.3.26",
"better-auth@<1.4.5": ">=1.4.5",
"qs@<6.14.1": ">=6.14.1",
- "react-router@>=7.0.0 <=7.11.0": ">=7.12.0",
- "react-router@>=7.0.0 <7.12.0": ">=7.12.0",
- "react-router@>=7.0.0 <7.9.6": ">=7.9.6",
- "@react-router/node@>=7.0.0 <=7.9.3": ">=7.9.4",
- "react-router@>=7.0.0 <=7.8.2": ">=7.9.0",
"diff@<8.0.3": ">=8.0.3",
"tar@<=7.5.2": ">=7.5.3",
"minimatch@<10.2.1": ">=10.2.1",
@@ -89,7 +94,7 @@
},
"devDependencies": {
"@eslint/js": "^9.32.0",
- "@react-router/dev": "^7.7.1",
+ "@react-router/dev": "^7.14.2",
"@tailwindcss/vite": "^4.1.11",
"@types/express": "^5.0.3",
"@types/multer": "^2.0.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index db5f1844..ef7e58c9 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -6,7 +6,7 @@ settings:
overrides:
webpack: '>=5.104.1'
- zod: 4.0.9
+ zod: ^4.4.2
vite@>=7.0.0 <=7.0.7: '>=7.0.8'
js-yaml@>=4.0.0 <4.1.1: '>=4.1.1'
glob@>=10.2.0 <10.5.0: '>=10.5.0'
@@ -16,11 +16,6 @@ overrides:
better-auth@<1.3.26: '>=1.3.26'
better-auth@<1.4.5: '>=1.4.5'
qs@<6.14.1: '>=6.14.1'
- react-router@>=7.0.0 <=7.11.0: '>=7.12.0'
- react-router@>=7.0.0 <7.12.0: '>=7.12.0'
- react-router@>=7.0.0 <7.9.6: '>=7.9.6'
- '@react-router/node@>=7.0.0 <=7.9.3': '>=7.9.4'
- react-router@>=7.0.0 <=7.8.2: '>=7.9.0'
diff@<8.0.3: '>=8.0.3'
tar@<=7.5.2: '>=7.5.3'
minimatch@<10.2.1: '>=10.2.1'
@@ -30,6 +25,15 @@ importers:
.:
dependencies:
+ '@aws-sdk/client-s3':
+ specifier: ^3
+ version: 3.1041.0
+ '@aws-sdk/lib-storage':
+ specifier: ^3
+ version: 3.1041.0(@aws-sdk/client-s3@3.1041.0)
+ '@aws-sdk/s3-request-presigner':
+ specifier: ^3
+ version: 3.1041.0
'@radix-ui/react-alert-dialog':
specifier: ^1.1.15
version: 1.1.15(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
@@ -69,15 +73,12 @@ importers:
'@radix-ui/react-tooltip':
specifier: ^1.2.7
version: 1.2.7(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
- '@react-oauth/google':
- specifier: ^0.13.4
- version: 0.13.4(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
'@react-router/node':
- specifier: ^7.9.5
- version: 7.12.0(react-router@7.12.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(typescript@5.8.3)
+ specifier: ^7.14.2
+ version: 7.14.2(react-router@7.14.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(typescript@5.8.3)
'@react-router/serve':
- specifier: ^7.7.1
- version: 7.7.1(react-router@7.12.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(typescript@5.8.3)
+ specifier: ^7.14.2
+ version: 7.14.2(react-router@7.14.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(typescript@5.8.3)
'@remotion/bundler':
specifier: 4.0.329
version: 4.0.329(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
@@ -108,6 +109,12 @@ importers:
axios:
specifier: ^1.13.5
version: 1.13.5
+ better-auth:
+ specifier: ^1.6.9
+ version: 1.6.9(pg@8.20.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
+ bullmq:
+ specifier: ^5.76.5
+ version: 5.76.5
class-variance-authority:
specifier: ^0.7.1
version: 0.7.1
@@ -126,6 +133,9 @@ importers:
framer-motion:
specifier: ^12.23.12
version: 12.23.12(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
+ ioredis:
+ specifier: ^5.10.1
+ version: 5.10.1
isbot:
specifier: ^5.1.29
version: 5.1.29
@@ -138,6 +148,9 @@ importers:
next-themes:
specifier: ^0.4.6
version: 0.4.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
+ pg:
+ specifier: ^8.20.0
+ version: 8.20.0
react:
specifier: ^19.1.1
version: 19.1.1
@@ -151,8 +164,8 @@ importers:
specifier: ^3.0.4
version: 3.0.4(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
react-router:
- specifier: ^7.12.0
- version: 7.12.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
+ specifier: ^7.14.2
+ version: 7.14.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
remotion:
specifier: 4.0.329
version: 4.0.329(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
@@ -165,13 +178,16 @@ importers:
vaul:
specifier: ^1.1.2
version: 1.1.2(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
+ zod:
+ specifier: ^4.4.2
+ version: 4.4.2
devDependencies:
'@eslint/js':
specifier: ^9.32.0
version: 9.32.0
'@react-router/dev':
- specifier: ^7.7.1
- version: 7.7.1(@react-router/serve@7.7.1(react-router@7.12.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(typescript@5.8.3))(@types/node@24.1.0)(jiti@2.5.1)(lightningcss@1.30.1)(react-router@7.12.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(terser@5.43.1)(tsx@4.20.4)(typescript@5.8.3)(vite@7.3.1(@types/node@24.1.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4))
+ specifier: ^7.14.2
+ version: 7.14.2(@react-router/serve@7.14.2(react-router@7.14.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(typescript@5.8.3))(@types/node@24.1.0)(jiti@2.5.1)(lightningcss@1.30.1)(react-router@7.14.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(terser@5.43.1)(tsx@4.20.4)(typescript@5.8.3)(vite@7.3.1(@types/node@24.1.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4))
'@tailwindcss/vite':
specifier: ^4.1.11
version: 4.1.11(vite@7.3.1(@types/node@24.1.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4))
@@ -242,6 +258,179 @@ packages:
resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
engines: {node: '>=6.0.0'}
+ '@aws-crypto/crc32@5.2.0':
+ resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==}
+ engines: {node: '>=16.0.0'}
+
+ '@aws-crypto/crc32c@5.2.0':
+ resolution: {integrity: sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==}
+
+ '@aws-crypto/sha1-browser@5.2.0':
+ resolution: {integrity: sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==}
+
+ '@aws-crypto/sha256-browser@5.2.0':
+ resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==}
+
+ '@aws-crypto/sha256-js@5.2.0':
+ resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==}
+ engines: {node: '>=16.0.0'}
+
+ '@aws-crypto/supports-web-crypto@5.2.0':
+ resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==}
+
+ '@aws-crypto/util@5.2.0':
+ resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==}
+
+ '@aws-sdk/client-s3@3.1041.0':
+ resolution: {integrity: sha512-sQV14bIqslnBHuSlLMD+fc3pH+ajop6vnrFlJ4wM4JDqcYwVik4O+9srnZUrkesFw5y+CN0GfOQ06CAgtC4mjQ==}
+ engines: {node: '>=20.0.0'}
+
+ '@aws-sdk/core@3.974.8':
+ resolution: {integrity: sha512-njR2qoG6ZuB0kvAS2FyICsFZJ6gmCcf2X/7JcD14sUvGDm26wiZ5BrA6LOiUxKFEF+IVe7kdroxyE00YlkiYsw==}
+ engines: {node: '>=20.0.0'}
+
+ '@aws-sdk/crc64-nvme@3.972.7':
+ resolution: {integrity: sha512-QUagVVBbC8gODCF6e1aV0mE2TXWB9Opz4k8EJFdNrujUVQm5R4AjJa1mpOqzwOuROBzqJU9zawzig7M96L8Ejg==}
+ engines: {node: '>=20.0.0'}
+
+ '@aws-sdk/credential-provider-env@3.972.34':
+ resolution: {integrity: sha512-XT0jtf8Fw9JE6ppsQeoNnZRiG+jqRixMT1v1ZR17G60UvVdsQmTG8nbEyHuEPfMxDXEhfdARaM/XiEhca4lGHQ==}
+ engines: {node: '>=20.0.0'}
+
+ '@aws-sdk/credential-provider-http@3.972.36':
+ resolution: {integrity: sha512-DPoGWfy7J7RKxvbf5kOKIGQkD2ek3dbKgzKIGrnLuvZBz5myU+Im/H6pmc14QcnFbqHMqxvtWSgRDSJW3qXLQg==}
+ engines: {node: '>=20.0.0'}
+
+ '@aws-sdk/credential-provider-ini@3.972.38':
+ resolution: {integrity: sha512-oDzUBu2MGJFgoar05sPMCwSrhw44ASyccrHzj66vO69OZqi7I6hZZxXfuPLC8OCzW7C+sU+bI73XHij41yekgQ==}
+ engines: {node: '>=20.0.0'}
+
+ '@aws-sdk/credential-provider-login@3.972.38':
+ resolution: {integrity: sha512-g1NosS8qe4OF++G2UFCM5ovSkgipC7YYor5KCWatG0UoMSO5YFj9C8muePlyVmOBV/WTI16Jo3/s1NUo/o1Bww==}
+ engines: {node: '>=20.0.0'}
+
+ '@aws-sdk/credential-provider-node@3.972.39':
+ resolution: {integrity: sha512-HEswDQyxUtadoZ/bJsPPENHg7R0Lzym5LuMksJeHvqhCOpP+rtkDLKI4/ZChH4w3cf5kG8n6bZuI8PzajoiqMg==}
+ engines: {node: '>=20.0.0'}
+
+ '@aws-sdk/credential-provider-process@3.972.34':
+ resolution: {integrity: sha512-T3IFs4EVmVi1dVN5RciFnklCANSzvrQd/VuHY9ThHSQmYkTogjcGkoJEr+oNUPQZnso52183088NqysMPji1/Q==}
+ engines: {node: '>=20.0.0'}
+
+ '@aws-sdk/credential-provider-sso@3.972.38':
+ resolution: {integrity: sha512-5ZxG+t0+3Q3QPh8KEjX6syskhgNf7I0MN7oGioTf6Lm1NTjfP7sIcYGNsthXC2qR8vcD3edNZwCr2ovfSSWuRA==}
+ engines: {node: '>=20.0.0'}
+
+ '@aws-sdk/credential-provider-web-identity@3.972.38':
+ resolution: {integrity: sha512-lYHFF30DGI20jZcYX8cm6Ns0V7f1dDN6g/MBDLTyD/5iw+bXs3yBr2iAiHDkx4RFU5JgsnZvCHYKiRVPRdmOgw==}
+ engines: {node: '>=20.0.0'}
+
+ '@aws-sdk/lib-storage@3.1041.0':
+ resolution: {integrity: sha512-kDJVrZTzRdeFFEppKQVbXzXOCwEzxUsBGIblH0OaeJbaOV5//ZphqxhznMd3QWckqicbIuShJWkmnQeBt+VmBw==}
+ engines: {node: '>=20.0.0'}
+ peerDependencies:
+ '@aws-sdk/client-s3': ^3.1041.0
+
+ '@aws-sdk/middleware-bucket-endpoint@3.972.10':
+ resolution: {integrity: sha512-Vbc2frZH7wXlMNd+ZZSXUEs/l1Sv8Jj4zUnIfwrYF5lwaLdXHZ9xx4U3rjUcaye3HRhFVc+E5DbBxpRAbB16BA==}
+ engines: {node: '>=20.0.0'}
+
+ '@aws-sdk/middleware-expect-continue@3.972.10':
+ resolution: {integrity: sha512-2Yn0f1Qiq/DjxYR3wfI3LokXnjOhFM7Ssn4LTdFDIxRMCE6I32MAsVnhPX1cUZsuVA9tiZtwwhlSLAtFGxAZlQ==}
+ engines: {node: '>=20.0.0'}
+
+ '@aws-sdk/middleware-flexible-checksums@3.974.16':
+ resolution: {integrity: sha512-6ru8doI0/XzszqLIPXf0E/V7HhAw1Pu94010XCKYtBUfD0LxF0BuOzrUf8OQGR6j2o6wgKTHUniOmndQycHwCA==}
+ engines: {node: '>=20.0.0'}
+
+ '@aws-sdk/middleware-host-header@3.972.10':
+ resolution: {integrity: sha512-IJSsIMeVQ8MMCPbuh1AbltkFhLBLXn7aejzfX5YKT/VLDHn++Dcz8886tXckE+wQssyPUhaXrJhdakO2VilRhg==}
+ engines: {node: '>=20.0.0'}
+
+ '@aws-sdk/middleware-location-constraint@3.972.10':
+ resolution: {integrity: sha512-rI3NZvJcEvjoD0+0PI0iUAwlPw2IlSlhyvgBK/3WkKJQE/YiKFedd9dMN2lVacdNxPNhxL/jzQaKQdrGtQagjQ==}
+ engines: {node: '>=20.0.0'}
+
+ '@aws-sdk/middleware-logger@3.972.10':
+ resolution: {integrity: sha512-OOuGvvz1Dm20SjZo5oEBePFqxt5nf8AwkNDSyUHvD9/bfNASmstcYxFAHUowy4n6Io7mWUZ04JURZwSBvyQanQ==}
+ engines: {node: '>=20.0.0'}
+
+ '@aws-sdk/middleware-recursion-detection@3.972.11':
+ resolution: {integrity: sha512-+zz6f79Kj9V5qFK2P+D8Ehjnw4AhphAlCAsPjUqEcInA9umtSSKMrHbSagEeOIsDNuvVrH98bjRHcyQukTrhaQ==}
+ engines: {node: '>=20.0.0'}
+
+ '@aws-sdk/middleware-sdk-s3@3.972.37':
+ resolution: {integrity: sha512-Km7M+i8DrLArVzrid1gfxeGhYHBd3uxvE77g0s5a52zPSVosxzQBnJ0gwWb6NIp/DOk8gsBMhi7V+cpJG0ndTA==}
+ engines: {node: '>=20.0.0'}
+
+ '@aws-sdk/middleware-ssec@3.972.10':
+ resolution: {integrity: sha512-Gli9A0u8EVVb+5bFDGS/QbSVg28w/wpEidg1ggVcSj65BDTdGR6punsOcVjqdiu1i42WHWo51MCvARPIIz9juw==}
+ engines: {node: '>=20.0.0'}
+
+ '@aws-sdk/middleware-user-agent@3.972.38':
+ resolution: {integrity: sha512-iz+B29TXcAZsJpwB+AwG/TTGA5l/VnmMZ2UxtiySOZjI6gCdmviXPwdgzcmuazMy16rXoPY4mYCGe7zdNKfx5A==}
+ engines: {node: '>=20.0.0'}
+
+ '@aws-sdk/nested-clients@3.997.6':
+ resolution: {integrity: sha512-WBDnqatJl+kGObpfmfSxqnXeYTu3Me8wx8WCtvoxX3pfWrrTv8I4WTMSSs7PZqcRcVh8WeUKMgGFjMG+52SR1w==}
+ engines: {node: '>=20.0.0'}
+
+ '@aws-sdk/region-config-resolver@3.972.13':
+ resolution: {integrity: sha512-CvJ2ZIjK/jVD/lbOpowBVElJyC1YxLTIJ13yM0AEo0t2v7swOzGjSA6lJGH+DwZXQhcjUjoYwc8bVYCX5MDr1A==}
+ engines: {node: '>=20.0.0'}
+
+ '@aws-sdk/s3-request-presigner@3.1041.0':
+ resolution: {integrity: sha512-DlKsPQ8Z75wgeDSHbjUPNDQCYUF0OLBkqllZqFei61KIoQDqEeKUCwuCf6RhNLjaP4b8oSpBA9+FmUS+zm3xUg==}
+ engines: {node: '>=20.0.0'}
+
+ '@aws-sdk/signature-v4-multi-region@3.996.25':
+ resolution: {integrity: sha512-+CMIt3e1VzlklAECmG+DtP1sV8iKq25FuA0OKpnJ4KA0kxUtd7CgClY7/RU6VzJBQwbN4EJ9Ue6plvqx1qGadw==}
+ engines: {node: '>=20.0.0'}
+
+ '@aws-sdk/token-providers@3.1041.0':
+ resolution: {integrity: sha512-Th7kPI6YPtvJUcdznooXJMy+9rQWjmEF81LxaJssngBzuysK4a/x+l8kjm1zb7nYsUPbndnBdUnwng/3PLvtGw==}
+ engines: {node: '>=20.0.0'}
+
+ '@aws-sdk/types@3.973.8':
+ resolution: {integrity: sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw==}
+ engines: {node: '>=20.0.0'}
+
+ '@aws-sdk/util-arn-parser@3.972.3':
+ resolution: {integrity: sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA==}
+ engines: {node: '>=20.0.0'}
+
+ '@aws-sdk/util-endpoints@3.996.8':
+ resolution: {integrity: sha512-oOZHcRDihk5iEe5V25NVWg45b3qEA8OpHWVdU/XQh8Zj4heVPAJqWvMphQnU7LkufmUo10EpvFPZuQMiFLJK3g==}
+ engines: {node: '>=20.0.0'}
+
+ '@aws-sdk/util-format-url@3.972.10':
+ resolution: {integrity: sha512-DEKiHNJVtNxdyTeQspzY+15Po/kHm6sF0Cs4HV9Q2+lplB63+DrvdeiSoOSdWEWAoO2RcY1veoXVDz2tWxWCgQ==}
+ engines: {node: '>=20.0.0'}
+
+ '@aws-sdk/util-locate-window@3.965.5':
+ resolution: {integrity: sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==}
+ engines: {node: '>=20.0.0'}
+
+ '@aws-sdk/util-user-agent-browser@3.972.10':
+ resolution: {integrity: sha512-FAzqXvfEssGdSIz8ejatan0bOdx1qefBWKF/gWmVBXIP1HkS7v/wjjaqrAGGKvyihrXTXW00/2/1nTJtxpXz7g==}
+
+ '@aws-sdk/util-user-agent-node@3.973.24':
+ resolution: {integrity: sha512-ZWwlkjcIp7cEL8ZfTpTAPNkwx25p7xol0xlKoWVVf22+nsjwmLcHYtTPjIV1cSpmB/b6DaK4cb1fSkvCXHgRdw==}
+ engines: {node: '>=20.0.0'}
+ peerDependencies:
+ aws-crt: '>=1.0.0'
+ peerDependenciesMeta:
+ aws-crt:
+ optional: true
+
+ '@aws-sdk/xml-builder@3.972.22':
+ resolution: {integrity: sha512-PMYKKtJd70IsSG0yHrdAbxBr+ZWBKLvzFZfD3/urxgf6hXVMzuU5M+3MJ5G67RpOmLBu1fAUN65SbWuKUCOlAA==}
+ engines: {node: '>=20.0.0'}
+
+ '@aws/lambda-invoke-store@0.2.4':
+ resolution: {integrity: sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==}
+ engines: {node: '>=18.0.0'}
+
'@babel/code-frame@7.27.1':
resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
engines: {node: '>=6.9.0'}
@@ -376,6 +565,85 @@ packages:
resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==}
engines: {node: '>=6.9.0'}
+ '@better-auth/core@1.6.9':
+ resolution: {integrity: sha512-ADFk5pwmLybmc+LvYvXJ6M1x2oY/EyYLkwLuH0x28FUq12DfjL0wnE7g+WRDf3yozDO+qIxTpFGXDGwLKbfz0w==}
+ peerDependencies:
+ '@better-auth/utils': 0.4.0
+ '@better-fetch/fetch': 1.1.21
+ '@cloudflare/workers-types': '>=4'
+ '@opentelemetry/api': ^1.9.0
+ better-call: 1.3.5
+ jose: ^6.1.0
+ kysely: ^0.28.5
+ nanostores: ^1.0.1
+ peerDependenciesMeta:
+ '@cloudflare/workers-types':
+ optional: true
+ '@opentelemetry/api':
+ optional: true
+
+ '@better-auth/drizzle-adapter@1.6.9':
+ resolution: {integrity: sha512-Lcco5hOGrMgc4XKAkvB6x72eQm4wCcya8IevMg4wBHY9W9GVg8pu23rpRX6VsVQSO4Ux13S7lFwUWtF7/r9aKw==}
+ peerDependencies:
+ '@better-auth/core': ^1.6.9
+ '@better-auth/utils': 0.4.0
+ drizzle-orm: ^0.45.2
+ peerDependenciesMeta:
+ drizzle-orm:
+ optional: true
+
+ '@better-auth/kysely-adapter@1.6.9':
+ resolution: {integrity: sha512-gyjuuxJtZ4o9G9z9q4kqn24X2kvMSp7F+KHogYxF03SnXY/2WleAcuj57iC4wP3e9mGDbjPOrnM5K6Kr3Ktdpw==}
+ peerDependencies:
+ '@better-auth/core': ^1.6.9
+ '@better-auth/utils': 0.4.0
+ kysely: ^0.28.14
+ peerDependenciesMeta:
+ kysely:
+ optional: true
+
+ '@better-auth/memory-adapter@1.6.9':
+ resolution: {integrity: sha512-XmIG4tUnOXZ+KEcWjHUjOI9Z5donD09dC2t/AQTXifAUIqx7cySg86w0KTM09ArzAxRx1fCqO36Wkt5nULnrkQ==}
+ peerDependencies:
+ '@better-auth/core': ^1.6.9
+ '@better-auth/utils': 0.4.0
+
+ '@better-auth/mongo-adapter@1.6.9':
+ resolution: {integrity: sha512-h+AiRJ/TsBSi+ZDjySASBpbJ/9QCXBre34PSKgCz7QmTHrFM9Cg2EM4AM7LjR5lPXipEE+2rWPBc9wfnUBjhcw==}
+ peerDependencies:
+ '@better-auth/core': ^1.6.9
+ '@better-auth/utils': 0.4.0
+ mongodb: ^6.0.0 || ^7.0.0
+ peerDependenciesMeta:
+ mongodb:
+ optional: true
+
+ '@better-auth/prisma-adapter@1.6.9':
+ resolution: {integrity: sha512-XHks01ntK20orqK/jICq8wmEbJ/zT6dct49Fk8zTQKN9QNGDc+Ix5+7z/Kvui0DXGFf790GfvRozquzaLtXa8Q==}
+ peerDependencies:
+ '@better-auth/core': ^1.6.9
+ '@better-auth/utils': 0.4.0
+ '@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0
+ prisma: ^5.0.0 || ^6.0.0 || ^7.0.0
+ peerDependenciesMeta:
+ '@prisma/client':
+ optional: true
+ prisma:
+ optional: true
+
+ '@better-auth/telemetry@1.6.9':
+ resolution: {integrity: sha512-0u5zkhSCAQFoN3DHvUkLHOF6MBbVTDAa6mU8mhPwiysdz1x21vMzhzfaAKN/ZGWaQ09v91/F+2qu42G/bhUV4A==}
+ peerDependencies:
+ '@better-auth/core': ^1.6.9
+ '@better-auth/utils': 0.4.0
+ '@better-fetch/fetch': 1.1.21
+
+ '@better-auth/utils@0.4.0':
+ resolution: {integrity: sha512-RpMtLUIQAEWMgdPLNVbIF5ON2mm+CH0U3rCdUCU1VyeAUui4m38DyK7/aXMLZov2YDjG684pS1D0MBllrmgjQA==}
+
+ '@better-fetch/fetch@1.1.21':
+ resolution: {integrity: sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A==}
+
'@cspotcode/source-map-support@0.8.1':
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
engines: {node: '>=12'}
@@ -915,6 +1183,9 @@ packages:
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
engines: {node: '>=18.18'}
+ '@ioredis/commands@1.5.1':
+ resolution: {integrity: sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==}
+
'@isaacs/fs-minipass@4.0.1':
resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==}
engines: {node: '>=18.0.0'}
@@ -941,6 +1212,47 @@ packages:
'@mjackson/node-fetch-server@0.2.0':
resolution: {integrity: sha512-EMlH1e30yzmTpGLQjlFmaDAjyOeZhng1/XCd7DExR8PNAnG/G1tyruZxEoUe11ClnwGhGrtsdnyyUx1frSzjng==}
+ '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3':
+ resolution: {integrity: sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3':
+ resolution: {integrity: sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3':
+ resolution: {integrity: sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3':
+ resolution: {integrity: sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==}
+ cpu: [arm]
+ os: [linux]
+
+ '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3':
+ resolution: {integrity: sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==}
+ cpu: [x64]
+ os: [linux]
+
+ '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3':
+ resolution: {integrity: sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==}
+ cpu: [x64]
+ os: [win32]
+
+ '@noble/ciphers@2.2.0':
+ resolution: {integrity: sha512-Z6pjIZ/8IJcCGzb2S/0Px5J81yij85xASuk1teLNeg75bfT07MV3a/O2Mtn1I2se43k3lkVEcFaR10N4cgQcZA==}
+ engines: {node: '>= 20.19.0'}
+
+ '@noble/hashes@2.2.0':
+ resolution: {integrity: sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==}
+ engines: {node: '>= 20.19.0'}
+
+ '@nodable/entities@2.1.0':
+ resolution: {integrity: sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==}
+
'@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
@@ -953,17 +1265,9 @@ packages:
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
engines: {node: '>= 8'}
- '@npmcli/git@4.1.0':
- resolution: {integrity: sha512-9hwoB3gStVfa0N31ymBmrX+GuDGdVA/QWShZVqE0HK2Af+7QGGrCTbZia/SW0ImUTjTne7SP91qxDmtXvDHRPQ==}
- engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
-
- '@npmcli/package-json@4.0.1':
- resolution: {integrity: sha512-lRCEGdHZomFsURroh522YvA/2cVb9oPIJrjHanCJZkiasz1BzcnLr3tBJhlV7S86MBJBuAQ33is2D60YitZL2Q==}
- engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
-
- '@npmcli/promise-spawn@6.0.2':
- resolution: {integrity: sha512-gGq0NJkIGSwdbUt4yhdF8ZrmkGKVz9vAdVzpOfnom+V8PLSmSOVhZwbNvZZS1EYcJN5hzzKBxmmVVAInM6HQLg==}
- engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
+ '@opentelemetry/semantic-conventions@1.40.0':
+ resolution: {integrity: sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==}
+ engines: {node: '>=14'}
'@radix-ui/number@1.1.1':
resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==}
@@ -1450,57 +1754,60 @@ packages:
'@radix-ui/rect@1.1.1':
resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
- '@react-oauth/google@0.13.4':
- resolution: {integrity: sha512-hGKyNEH+/PK8M0sFEuo3MAEk0txtHpgs94tDQit+s2LXg7b6z53NtzHfqDvoB2X8O6lGB+FRg80hY//X6hfD+w==}
- peerDependencies:
- react: '>=16.8.0'
- react-dom: '>=16.8.0'
-
- '@react-router/dev@7.7.1':
- resolution: {integrity: sha512-ByfgHmAyfx/JQYN/QwUx1sFJlBA5Z3HQAZ638wHSb+m6khWtHqSaKCvPqQh1P00wdEAeV3tX5L1aUM/ceCF6+w==}
+ '@react-router/dev@7.14.2':
+ resolution: {integrity: sha512-lU88Ls4iC78RdPOKkER54+hlsHzzS8WSZrf2/cGQumbIN2A5WvO0LDyv72cdJmLWujgZ9rpNoGzmqWINssShGQ==}
engines: {node: '>=20.0.0'}
hasBin: true
peerDependencies:
- '@react-router/serve': ^7.7.1
- react-router: '>=7.9.0'
- typescript: ^5.1.0
+ '@react-router/serve': ^7.14.2
+ '@vitejs/plugin-rsc': ~0.5.21
+ react-router: ^7.14.2
+ react-server-dom-webpack: ^19.2.3
+ typescript: ^5.1.0 || ^6.0.0
vite: '>=7.0.8'
wrangler: ^3.28.2 || ^4.0.0
peerDependenciesMeta:
'@react-router/serve':
optional: true
+ '@vitejs/plugin-rsc':
+ optional: true
+ react-server-dom-webpack:
+ optional: true
typescript:
optional: true
wrangler:
optional: true
- '@react-router/express@7.7.1':
- resolution: {integrity: sha512-OEZwIM7i/KPSDjwVRg3LqeNIwG41U+SeFOwMjhZRFfyrnwghHfvWsDajf73r4ccMh+RRHcP1GIN6VSU3XZk7MA==}
+ '@react-router/express@7.14.2':
+ resolution: {integrity: sha512-IYs61kHfMWsJk/ju4Ts4hw7wblZecfXuIvqQPKEaz+gwpkJMSWDzhPpgmC16EnmBQkXPqMVpsjvNxA/d9p9ehg==}
engines: {node: '>=20.0.0'}
peerDependencies:
express: ^4.17.1 || ^5
- react-router: '>=7.9.0'
- typescript: ^5.1.0
+ react-router: 7.14.2
+ typescript: ^5.1.0 || ^6.0.0
peerDependenciesMeta:
typescript:
optional: true
- '@react-router/node@7.12.0':
- resolution: {integrity: sha512-o/t10Cse4LK8kFefqJ8JjC6Ng6YuKD2I87S2AiJs17YAYtXU5W731ZqB73AWyCDd2G14R0dSuqXiASRNK/xLjg==}
+ '@react-router/node@7.14.2':
+ resolution: {integrity: sha512-8zxVfgKOXjk0k8YxSBDTFyNAuVdr+og1wFbQpmJJOxo7ObxfI81EbHenyyxGvFiw77rNFLS9Dqgnv5xZgHZfCw==}
engines: {node: '>=20.0.0'}
peerDependencies:
- react-router: 7.12.0
- typescript: ^5.1.0
+ react-router: 7.14.2
+ typescript: ^5.1.0 || ^6.0.0
peerDependenciesMeta:
typescript:
optional: true
- '@react-router/serve@7.7.1':
- resolution: {integrity: sha512-LyAiX+oI+6O6j2xWPUoKW+cgayUf3USBosSMv73Jtwi99XUhSDu2MUhM+BB+AbrYRubauZ83QpZTROiXoaf8jA==}
+ '@react-router/serve@7.14.2':
+ resolution: {integrity: sha512-Rh/Mrd9+Jkf+IOd7beEccCfTDavOQRpkk0TLwLFK60dv0yUIyOTIaKxC7W6I0WMrgAjhUL09JxfMsoz2vtYhTg==}
engines: {node: '>=20.0.0'}
hasBin: true
peerDependencies:
- react-router: '>=7.9.0'
+ react-router: 7.14.2
+
+ '@remix-run/node-fetch-server@0.13.1':
+ resolution: {integrity: sha512-dOL+A/C84EA47gO/ps52KGrVSiYy96512rwtbXmJfWKYFm1FbrbjA3jao1hcIfao+jwVNEaZ1kTMwFjiino+HQ==}
'@remotion/bundler@4.0.329':
resolution: {integrity: sha512-9nNj8c/OOtoarh9fNSEn92ySJnBiFiLvrs9xkGdaot23eB9BnjUSCZZp5ei6B1IXKtofOoHdYeupLUHZ9dTlyA==}
@@ -1622,7 +1929,7 @@ packages:
'@remotion/zod-types@4.0.329':
resolution: {integrity: sha512-H4PlDi6XhgU2LdlQoToK6cYoXY+PhPAUJuaqiPWRh7ifxUGTubGC1fdiN683L5a+ZxyV2IfNOTj0Q2uRwtGSZA==}
peerDependencies:
- zod: 4.0.9
+ zod: ^4.4.2
'@rollup/rollup-android-arm-eabi@4.50.1':
resolution: {integrity: sha512-HJXwzoZN4eYTdD8bVV22DN8gsPCAj3V20NHKOs8ezfXanGpmVPR7kalUHd+Y31IJp9stdB87VKPFbsGY3H/2ag==}
@@ -1740,6 +2047,221 @@ packages:
cpu: [x64]
os: [win32]
+ '@smithy/chunked-blob-reader-native@4.2.3':
+ resolution: {integrity: sha512-jA5k5Udn7Y5717L86h4EIv06wIr3xn8GM1qHRi/Nf31annXcXHJjBKvgztnbn2TxH3xWrPBfgwHsOwZf0UmQWw==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/chunked-blob-reader@5.2.2':
+ resolution: {integrity: sha512-St+kVicSyayWQca+I1rGitaOEH6uKgE8IUWoYnnEX26SWdWQcL6LvMSD19Lg+vYHKdT9B2Zuu7rd3i6Wnyb/iw==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/config-resolver@4.4.17':
+ resolution: {integrity: sha512-TzDZcAnhTyAHbXVxWZo7/tEcrIeFq20IBk8So3OLOetWpR8EwY/yEqBMBFaJMeyEiREDq4NfEl+qO3OAUD+vbQ==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/core@3.23.17':
+ resolution: {integrity: sha512-x7BlLbUFL8NWCGjMF9C+1N5cVCxcPa7g6Tv9B4A2luWx3be3oU8hQ96wIwxe/s7OhIzvoJH73HAUSg5JXVlEtQ==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/credential-provider-imds@4.2.14':
+ resolution: {integrity: sha512-Au28zBN48ZAoXdooGUHemuVBrkE+Ie6RPmGNIAJsFqj33Vhb6xAgRifUydZ2aY+M+KaMAETAlKk5NC5h1G7wpg==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/eventstream-codec@4.2.14':
+ resolution: {integrity: sha512-erZq0nOIpzfeZdCyzZjdJb4nVSKLUmSkaQUVkRGQTXs30gyUGeKnrYEg+Xe1W5gE3aReS7IgsvANwVPxSzY6Pw==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/eventstream-serde-browser@4.2.14':
+ resolution: {integrity: sha512-8IelTCtTctWRbb+0Dcy+C0aICh1qa0qWXqgjcXDmMuCvPJRnv26hiDZoAau2ILOniki65mCPKqOQs/BaWvO4CQ==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/eventstream-serde-config-resolver@4.3.14':
+ resolution: {integrity: sha512-sqHiHpYRYo3FJlaIxD1J8PhbcmJAm7IuM16mVnwSkCToD7g00IBZzKuiLNMGmftULmEUX6/UAz8/NN5uMP8bVA==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/eventstream-serde-node@4.2.14':
+ resolution: {integrity: sha512-Ht/8BuGlKfFTy0H3+8eEu0vdpwGztCnaLLXtpXNdQqiR7Hj4vFScU3T436vRAjATglOIPjJXronY+1WxxNLSiw==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/eventstream-serde-universal@4.2.14':
+ resolution: {integrity: sha512-lWyt4T2XQZUZgK3tQ3Wn0w3XBvZsK/vjTuJl6bXbnGZBHH0ZUSONTYiK9TgjTTzU54xQr3DRFwpjmhp0oLm3gg==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/fetch-http-handler@5.3.17':
+ resolution: {integrity: sha512-bXOvQzaSm6MnmLaWA1elgfQcAtN4UP3vXqV97bHuoOrHQOJiLT3ds6o9eo5bqd0TJfRFpzdGnDQdW3FACiAVdw==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/hash-blob-browser@4.2.15':
+ resolution: {integrity: sha512-0PJ4Al3fg2nM4qKrAIxyNcApgqHAXcBkN8FeizOz69z0rb26uZ6lMESYtxegaTlXB5Hj84JfwMPavMrwDMjucA==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/hash-node@4.2.14':
+ resolution: {integrity: sha512-8ZBDY2DD4wr+GGjTpPtiglEsqr0lUP+KHqgZcWczFf6qeZ/YRjMIOoQWVQlmwu7EtxKTd8YXD8lblmYcpBIA1g==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/hash-stream-node@4.2.14':
+ resolution: {integrity: sha512-tw4GANWkZPb6+BdD4Fgucqzey2+r73Z/GRo9zklsCdwrnxxumUV83ZIaBDdudV4Ylazw3EPTiJZhpX42105ruQ==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/invalid-dependency@4.2.14':
+ resolution: {integrity: sha512-c21qJiTSb25xvvOp+H2TNZzPCngrvl5vIPqPB8zQ/DmJF4QWXO19x1dWfMJZ6wZuuWUPPm0gV8C0cU3+ifcWuw==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/is-array-buffer@2.2.0':
+ resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==}
+ engines: {node: '>=14.0.0'}
+
+ '@smithy/is-array-buffer@4.2.2':
+ resolution: {integrity: sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/md5-js@4.2.14':
+ resolution: {integrity: sha512-V2v0vx+h0iUSNG1Alt+GNBMSLGCrl9iVsdd+Ap67HPM9PN479x12V8LkuMoKImNZxn3MXeuyUjls+/7ZACZghA==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/middleware-content-length@4.2.14':
+ resolution: {integrity: sha512-xhHq7fX4/3lv5NHxLUk3OeEvl0xZ+Ek3qIbWaCL4f9JwgDZEclPBElljaZCAItdGPQl/kSM4LPMOpy1MYgprpw==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/middleware-endpoint@4.4.32':
+ resolution: {integrity: sha512-ZZkgyjnJppiZbIm6Qbx92pbXYi1uzenIvGhBSCDlc7NwuAkiqSgS75j1czAD25ZLs2FjMjYy1q7gyRVWG6JA0Q==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/middleware-retry@4.5.7':
+ resolution: {integrity: sha512-bRt6ZImqVSeTk39Nm81K20ObIiAZ3WefY7G6+iz/0tZjs4dgRRjvRX2sgsH+zi6iDCRR/aQvQofLKxxz4rPBZg==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/middleware-serde@4.2.20':
+ resolution: {integrity: sha512-Lx9JMO9vArPtiChE3wbEZ5akMIDQpWQtlu90lhACQmNOXcGXRbaDywMHDzuDZ2OkZzP+9wQfZi3YJT9F67zTQQ==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/middleware-stack@4.2.14':
+ resolution: {integrity: sha512-2dvkUKLuFdKsCRmOE4Mn63co0Djtsm+JMh0bYZQupN1pJwMeE8FmQmRLLzzEMN0dnNi7CDCYYH8F0EVwWiPBeA==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/node-config-provider@4.3.14':
+ resolution: {integrity: sha512-S+gFjyo/weSVL0P1b9Ts8C/CwIfNCgUPikk3sl6QVsfE/uUuO+QsF+NsE/JkpvWqqyz1wg7HFdiaZuj5CoBMRg==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/node-http-handler@4.6.1':
+ resolution: {integrity: sha512-iB+orM4x3xrr57X3YaXazfKnntl0LHlZB1kcXSGzMV1Tt0+YwEjGlbjk/44qEGtBzXAz6yFDzkYTKSV6Pj2HUg==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/property-provider@4.2.14':
+ resolution: {integrity: sha512-WuM31CgfsnQ/10i7NYr0PyxqknD72Y5uMfUMVSniPjbEPceiTErb4eIqJQ+pdxNEAUEWrewrGjIRjVbVHsxZiQ==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/protocol-http@5.3.14':
+ resolution: {integrity: sha512-dN5F8kHx8RNU0r+pCwNmFZyz6ChjMkzShy/zup6MtkRmmix4vZzJdW+di7x//b1LiynIev88FM18ie+wwPcQtQ==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/querystring-builder@4.2.14':
+ resolution: {integrity: sha512-XYA5Z0IqTeF+5XDdh4BBmSA0HvbgVZIyv4cmOoUheDNR57K1HgBp9ukUMx3Cr3XpDHHpLBnexPE3LAtDsZkj2A==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/querystring-parser@4.2.14':
+ resolution: {integrity: sha512-hr+YyqBD23GVvRxGGrcc/oOeNlK3PzT5Fu4dzrDXxzS1LpFiuL2PQQqKPs87M79aW7ziMs+nvB3qdw77SqE7Lw==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/service-error-classification@4.3.1':
+ resolution: {integrity: sha512-aUQuDGh760ts/8MU+APjIZhlLPKhIIfqyzZaJikLEIMrdxFvxuLYD0WxWzaYWpmLbQlXDe9p7EWM3HsBe0K6Gw==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/shared-ini-file-loader@4.4.9':
+ resolution: {integrity: sha512-495/V2I15SHgedSJoDPD23JuSfKAp726ZI1V0wtjB07Wh7q/0tri/0e0DLefZCHgxZonrGKt/OCTpAtP1wE1kQ==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/signature-v4@5.3.14':
+ resolution: {integrity: sha512-1D9Y/nmlVjCeSivCbhZ7hgEpmHyY1h0GvpSZt3l0xcD9JjmjVC1CHOozS6+Gh+/ldMH8JuJ6cujObQqfayAVFA==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/smithy-client@4.12.13':
+ resolution: {integrity: sha512-y/Pcj1V9+qG98gyu1gvftHB7rDpdh+7kIBIggs55yGm3JdtBV8GT8IFF3a1qxZ79QnaJHX9GXzvBG6tAd+czJA==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/types@4.14.1':
+ resolution: {integrity: sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/url-parser@4.2.14':
+ resolution: {integrity: sha512-p06BiBigJ8bTA3MgnOfCtDUWnAMY0YfedO/GRpmc7p+wg3KW8vbXy1xwSu5ASy0wV7rRYtlfZOIKH4XqfhjSQQ==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/util-base64@4.3.2':
+ resolution: {integrity: sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/util-body-length-browser@4.2.2':
+ resolution: {integrity: sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/util-body-length-node@4.2.3':
+ resolution: {integrity: sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/util-buffer-from@2.2.0':
+ resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==}
+ engines: {node: '>=14.0.0'}
+
+ '@smithy/util-buffer-from@4.2.2':
+ resolution: {integrity: sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/util-config-provider@4.2.2':
+ resolution: {integrity: sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/util-defaults-mode-browser@4.3.49':
+ resolution: {integrity: sha512-a5bNrdiONYB/qE2BuKegvUMd/+ZDwdg4vsNuuSzYE8qs2EYAdK9CynL+Rzn29PbPiUqoz/cbpRbcLzD5lEevHw==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/util-defaults-mode-node@4.2.54':
+ resolution: {integrity: sha512-g1cvrJvOnzeJgEdf7AE4luI7gp6L8weE0y9a9wQUSGtjb8QRHDbCJYuE4Sy0SD9N8RrnNPFsPltAz/OSoBR9Zw==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/util-endpoints@3.4.2':
+ resolution: {integrity: sha512-a55Tr+3OKld4TTtnT+RhKOQHyPxm3j/xL4OR83WBUhLJaKDS9dnJ7arRMOp3t31dcLhApwG9bgvrRXBHlLdIkg==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/util-hex-encoding@4.2.2':
+ resolution: {integrity: sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/util-middleware@4.2.14':
+ resolution: {integrity: sha512-1Su2vj9RYNDEv/V+2E+jXkkwGsgR7dc4sfHn9Z7ruzQHJIEni9zzw5CauvRXlFJfmgcqYP8fWa0dkh2Q2YaQyw==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/util-retry@4.3.8':
+ resolution: {integrity: sha512-LUIxbTBi+OpvXpg91poGA6BdyoleMDLnfXjVDqyi2RvZmTveY5loE/FgYUBCR5LU2BThW2SoZRh8dTIIy38IPw==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/util-stream@4.5.25':
+ resolution: {integrity: sha512-/PFpG4k8Ze8Ei+mMKj3oiPICYekthuzePZMgZbCqMiXIHHf4n2aZ4Ps0aSRShycFTGuj/J6XldmC0x0DwednIA==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/util-uri-escape@4.2.2':
+ resolution: {integrity: sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/util-utf8@2.3.0':
+ resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==}
+ engines: {node: '>=14.0.0'}
+
+ '@smithy/util-utf8@4.2.2':
+ resolution: {integrity: sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/util-waiter@4.3.0':
+ resolution: {integrity: sha512-JyjYmLAfS+pdxF92o4yLgEoy0zhayKTw73FU1aofLWwLcJw7iSqIY2exGmMTrl/lmZugP5p/zxdFSippJDfKWA==}
+ engines: {node: '>=18.0.0'}
+
+ '@smithy/uuid@1.1.2':
+ resolution: {integrity: sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g==}
+ engines: {node: '>=18.0.0'}
+
+ '@standard-schema/spec@1.1.0':
+ resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
+
'@tailwindcss/node@4.1.11':
resolution: {integrity: sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==}
@@ -2149,6 +2671,9 @@ packages:
resolution: {integrity: sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==}
engines: {node: 20 || >=22}
+ base64-js@1.5.1:
+ resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
+
baseline-browser-mapping@2.9.19:
resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==}
hasBin: true
@@ -2157,10 +2682,80 @@ packages:
resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==}
engines: {node: '>= 0.8'}
- big.js@5.2.2:
- resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==}
-
- body-parser@1.20.3:
+ better-auth@1.6.9:
+ resolution: {integrity: sha512-EBFURtglyiEZxbx4NJBoqUD8J65dX24yC+6I9AUbIXNgUkt76mshzGbHkxZ3n/lB7Dwq3kBC+hHt0hUQsnL7HA==}
+ peerDependencies:
+ '@lynx-js/react': '*'
+ '@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0
+ '@sveltejs/kit': ^2.0.0
+ '@tanstack/react-start': ^1.0.0
+ '@tanstack/solid-start': ^1.0.0
+ better-sqlite3: ^12.0.0
+ drizzle-kit: '>=0.31.4'
+ drizzle-orm: ^0.45.2
+ mongodb: ^6.0.0 || ^7.0.0
+ mysql2: ^3.0.0
+ next: ^14.0.0 || ^15.0.0 || ^16.0.0
+ pg: ^8.0.0
+ prisma: ^5.0.0 || ^6.0.0 || ^7.0.0
+ react: ^18.0.0 || ^19.0.0
+ react-dom: ^18.0.0 || ^19.0.0
+ solid-js: ^1.0.0
+ svelte: ^4.0.0 || ^5.0.0
+ vitest: ^2.0.0 || ^3.0.0 || ^4.0.0
+ vue: ^3.0.0
+ peerDependenciesMeta:
+ '@lynx-js/react':
+ optional: true
+ '@prisma/client':
+ optional: true
+ '@sveltejs/kit':
+ optional: true
+ '@tanstack/react-start':
+ optional: true
+ '@tanstack/solid-start':
+ optional: true
+ better-sqlite3:
+ optional: true
+ drizzle-kit:
+ optional: true
+ drizzle-orm:
+ optional: true
+ mongodb:
+ optional: true
+ mysql2:
+ optional: true
+ next:
+ optional: true
+ pg:
+ optional: true
+ prisma:
+ optional: true
+ react:
+ optional: true
+ react-dom:
+ optional: true
+ solid-js:
+ optional: true
+ svelte:
+ optional: true
+ vitest:
+ optional: true
+ vue:
+ optional: true
+
+ better-call@1.3.5:
+ resolution: {integrity: sha512-kOFJkBP7utAQLEYrobZm3vkTH8mXq5GNgvjc5/XEST1ilVHaxXUXfeDeFlqoETMtyqS4+3/h4ONX2i++ebZrvA==}
+ peerDependencies:
+ zod: ^4.4.2
+ peerDependenciesMeta:
+ zod:
+ optional: true
+
+ big.js@5.2.2:
+ resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==}
+
+ body-parser@1.20.3:
resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==}
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
@@ -2168,6 +2763,9 @@ packages:
resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==}
engines: {node: '>=18'}
+ bowser@2.14.1:
+ resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==}
+
brace-expansion@5.0.2:
resolution: {integrity: sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==}
engines: {node: 20 || >=22}
@@ -2176,11 +2774,6 @@ packages:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
engines: {node: '>=8'}
- browserslist@4.25.1:
- resolution: {integrity: sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==}
- engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
- hasBin: true
-
browserslist@4.28.1:
resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==}
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
@@ -2192,6 +2785,13 @@ packages:
buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
+ buffer@5.6.0:
+ resolution: {integrity: sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==}
+
+ bullmq@5.76.5:
+ resolution: {integrity: sha512-2OKJP2+ckc+TygsWdxxeZYYgM9xYnVXgIAx+perflhamZ6FEBu/cSrvpqM8++fJI5OgsIFLfxA9UO7BDZ74Inw==}
+ engines: {node: '>=12.22.0'}
+
busboy@1.6.0:
resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
engines: {node: '>=10.16.0'}
@@ -2220,9 +2820,6 @@ packages:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
engines: {node: '>=6'}
- caniuse-lite@1.0.30001731:
- resolution: {integrity: sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==}
-
caniuse-lite@1.0.30001769:
resolution: {integrity: sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==}
@@ -2249,6 +2846,10 @@ packages:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
+ cluster-key-slot@1.1.2:
+ resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==}
+ engines: {node: '>=0.10.0'}
+
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
@@ -2275,6 +2876,9 @@ packages:
resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==}
engines: {'0': node >= 6.0}
+ confbox@0.2.4:
+ resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==}
+
content-disposition@0.5.4:
resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==}
engines: {node: '>= 0.6'}
@@ -2316,6 +2920,10 @@ packages:
create-require@1.1.1:
resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
+ cron-parser@4.9.0:
+ resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==}
+ engines: {node: '>=12.0.0'}
+
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
@@ -2395,10 +3003,17 @@ packages:
resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
engines: {node: '>= 0.4'}
+ defu@6.1.7:
+ resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==}
+
delayed-stream@1.0.0:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
+ denque@2.1.0:
+ resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==}
+ engines: {node: '>=0.10'}
+
depd@2.0.0:
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
engines: {node: '>= 0.8'}
@@ -2437,9 +3052,6 @@ packages:
ee-first@1.1.1:
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
- electron-to-chromium@1.5.192:
- resolution: {integrity: sha512-rP8Ez0w7UNw/9j5eSXCe10o1g/8B1P5SM90PCCMVkIRQn2R0LEHWz4Eh9RnxkniuDe1W0cTSOB3MLlkTGDcuCg==}
-
electron-to-chromium@1.5.286:
resolution: {integrity: sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==}
@@ -2466,9 +3078,6 @@ packages:
resolution: {integrity: sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==}
engines: {node: '>=10.13.0'}
- err-code@2.0.3:
- resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==}
-
es-abstract@1.24.0:
resolution: {integrity: sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==}
engines: {node: '>= 0.4'}
@@ -2624,6 +3233,9 @@ packages:
resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==}
engines: {node: '>= 18'}
+ exsolve@1.0.8:
+ resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==}
+
extract-zip@2.0.1:
resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==}
engines: {node: '>= 10.17.0'}
@@ -2645,6 +3257,13 @@ packages:
fast-uri@3.0.6:
resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==}
+ fast-xml-builder@1.1.5:
+ resolution: {integrity: sha512-4TJn/8FKLeslLAH3dnohXqE3QSoxkhvaMzepOIZytwJXZO69Bfz0HBdDHzOTOon6G59Zrk6VQ2bEiv1t61rfkA==}
+
+ fast-xml-parser@5.7.2:
+ resolution: {integrity: sha512-P7oW7tLbYnhOLQk/Gv7cZgzgMPP/XN03K02/Jy6Y/NHzyIAIpxuZIM/YqAkfiXFPxA2CTm7NtCijK9EDu09u2w==}
+ hasBin: true
+
fastq@1.19.1:
resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==}
@@ -2794,10 +3413,6 @@ packages:
glob-to-regexp@0.4.1:
resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==}
- glob@13.0.0:
- resolution: {integrity: sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==}
- engines: {node: 20 || >=22}
-
globals@14.0.0:
resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
engines: {node: '>=18'}
@@ -2846,10 +3461,6 @@ packages:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
- hosted-git-info@6.1.3:
- resolution: {integrity: sha512-HVJyzUrLIL1c0QmviVh5E8VGyUS7xCFPS6yydaVd1UegW+ibV/CohqTH9MkOLDp5o+rb82DMo77PTuc9F/8GKw==}
- engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
-
http-errors@2.0.0:
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
engines: {node: '>= 0.8'}
@@ -2876,6 +3487,9 @@ packages:
peerDependencies:
postcss: ^8.1.0
+ ieee754@1.2.1:
+ resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
+
ignore@5.3.2:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'}
@@ -2899,6 +3513,10 @@ packages:
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
engines: {node: '>= 0.4'}
+ ioredis@5.10.1:
+ resolution: {integrity: sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==}
+ engines: {node: '>=12.22.0'}
+
ipaddr.js@1.9.1:
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
engines: {node: '>= 0.10'}
@@ -3041,6 +3659,9 @@ packages:
resolution: {integrity: sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==}
hasBin: true
+ jose@6.2.3:
+ resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==}
+
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@@ -3059,10 +3680,6 @@ packages:
json-parse-even-better-errors@2.3.1:
resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
- json-parse-even-better-errors@3.0.2:
- resolution: {integrity: sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==}
- engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
-
json-schema-traverse@0.4.1:
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
@@ -3088,6 +3705,10 @@ packages:
resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
engines: {node: '>=6'}
+ kysely@0.28.16:
+ resolution: {integrity: sha512-3i5pmOiZvMDj00qhrIVbH0AnioVTx22DMP7Vn5At4yJO46iy+FM8Y/g61ltenLVSo3fiO8h8Q3QOFgf/gQ72ww==}
+ engines: {node: '>=20.0.0'}
+
levn@0.4.1:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'}
@@ -3172,6 +3793,12 @@ packages:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'}
+ lodash.defaults@4.2.0:
+ resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==}
+
+ lodash.isarguments@3.1.0:
+ resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==}
+
lodash.merge@4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
@@ -3185,10 +3812,6 @@ packages:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
hasBin: true
- lru-cache@11.2.4:
- resolution: {integrity: sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==}
- engines: {node: 20 || >=22}
-
lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
@@ -3196,15 +3819,15 @@ packages:
resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
engines: {node: '>=10'}
- lru-cache@7.18.3:
- resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==}
- engines: {node: '>=12'}
-
lucide-react@0.534.0:
resolution: {integrity: sha512-4Bz7rujQ/mXHqCwjx09ih/Q9SCizz9CjBV5repw9YSHZZZaop9/Oj0RgCDt6WdEaeAPfbcZ8l2b4jzApStqgNw==}
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ luxon@3.7.2:
+ resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==}
+ engines: {node: '>=12'}
+
magic-string@0.30.17:
resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
@@ -3326,6 +3949,13 @@ packages:
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+ msgpackr-extract@3.0.3:
+ resolution: {integrity: sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==}
+ hasBin: true
+
+ msgpackr@1.11.12:
+ resolution: {integrity: sha512-RBdJ1Un7yGlXWajrkxcSa93nvQ0w4zBf60c0yYv7YtBelP8H2FA7XsfBbMHtXKXUMUxH7zV3Zuozh+kUQWhHvg==}
+
multer@2.0.2:
resolution: {integrity: sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==}
engines: {node: '>= 10.16.0'}
@@ -3335,6 +3965,10 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
+ nanostores@1.3.0:
+ resolution: {integrity: sha512-XPUa/jz+P1oJvN9VBxw4L9MtdFfaH3DAryqPssqhb2kXjmb9npz0dly6rCsgFWOPr4Yg9mTfM3MDZgZZ+7A3lA==}
+ engines: {node: ^20.0.0 || >=22.0.0}
+
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
@@ -3359,32 +3993,16 @@ packages:
react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
- node-releases@2.0.19:
- resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
+ node-abort-controller@3.1.1:
+ resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==}
+
+ node-gyp-build-optional-packages@5.2.2:
+ resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==}
+ hasBin: true
node-releases@2.0.27:
resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
- normalize-package-data@5.0.0:
- resolution: {integrity: sha512-h9iPVIfrVZ9wVYQnxFgtw1ugSvGEMOlyPWWtm8BMJhnwyEL/FLbYbTY3V3PpjI/BUK67n9PEWDu6eHzu1fB15Q==}
- engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
-
- npm-install-checks@6.3.0:
- resolution: {integrity: sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==}
- engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
-
- npm-normalize-package-bin@3.0.1:
- resolution: {integrity: sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==}
- engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
-
- npm-package-arg@10.1.0:
- resolution: {integrity: sha512-uFyyCEmgBfZTtrKk/5xDfHp6+MdrqGotX/VoOyEEl3mBwiEE5FlBaePanazJSVMPT7vKepcjYBY2ztg9A3yPIA==}
- engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
-
- npm-pick-manifest@8.0.2:
- resolution: {integrity: sha512-1dKY+86/AIiq1tkKVD3l0WI+Gd3vkknVGAggsFeBkTvbhMQ1OND/LKkYv4JtXPKUJ8bOTCyLiqEg2P6QNdK+Gg==}
- engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
-
npm-run-path@4.0.1:
resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==}
engines: {node: '>=8'}
@@ -3456,6 +4074,10 @@ packages:
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
engines: {node: '>=10'}
+ p-map@7.0.4:
+ resolution: {integrity: sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==}
+ engines: {node: '>=18'}
+
parent-module@1.0.1:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'}
@@ -3468,6 +4090,10 @@ packages:
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
engines: {node: '>=8'}
+ path-expression-matcher@1.5.0:
+ resolution: {integrity: sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==}
+ engines: {node: '>=14.0.0'}
+
path-key@3.1.1:
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
engines: {node: '>=8'}
@@ -3475,10 +4101,6 @@ packages:
path-parse@1.0.7:
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
- path-scurry@2.0.1:
- resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==}
- engines: {node: 20 || >=22}
-
path-to-regexp@0.1.12:
resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==}
@@ -3495,17 +4117,43 @@ packages:
pend@1.2.0:
resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==}
+ pg-cloudflare@1.3.0:
+ resolution: {integrity: sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==}
+
+ pg-connection-string@2.12.0:
+ resolution: {integrity: sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==}
+
pg-int8@1.0.1:
resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==}
engines: {node: '>=4.0.0'}
+ pg-pool@3.13.0:
+ resolution: {integrity: sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==}
+ peerDependencies:
+ pg: '>=8.0'
+
pg-protocol@1.10.3:
resolution: {integrity: sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==}
+ pg-protocol@1.13.0:
+ resolution: {integrity: sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==}
+
pg-types@2.2.0:
resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==}
engines: {node: '>=4'}
+ pg@8.20.0:
+ resolution: {integrity: sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==}
+ engines: {node: '>= 16.0.0'}
+ peerDependencies:
+ pg-native: '>=3.0.1'
+ peerDependenciesMeta:
+ pg-native:
+ optional: true
+
+ pgpass@1.0.5:
+ resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==}
+
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@@ -3517,6 +4165,9 @@ packages:
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
engines: {node: '>=12'}
+ pkg-types@2.3.1:
+ resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==}
+
possible-typed-array-names@1.1.0:
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
engines: {node: '>= 0.4'}
@@ -3581,22 +4232,6 @@ packages:
engines: {node: '>=14'}
hasBin: true
- proc-log@3.0.0:
- resolution: {integrity: sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==}
- engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
-
- promise-inflight@1.0.1:
- resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==}
- peerDependencies:
- bluebird: '*'
- peerDependenciesMeta:
- bluebird:
- optional: true
-
- promise-retry@2.0.1:
- resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==}
- engines: {node: '>=10'}
-
prompts@2.4.2:
resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
engines: {node: '>= 6'}
@@ -3687,8 +4322,8 @@ packages:
react: ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
react-dom: ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
- react-router@7.12.0:
- resolution: {integrity: sha512-kTPDYPFzDVGIIGNLS5VJykK0HfHLY5MF3b+xj0/tTyNYL1gF1qs7u67Z9jEhQk2sQ98SUaHxlG31g1JtF7IfVw==}
+ react-router@7.14.2:
+ resolution: {integrity: sha512-yCqNne6I8IB6rVCH7XUvlBK7/QKyqypBFGv+8dj4QBFJiiRX+FG7/nkdAvGElyvVZ/HQP5N19wzteuTARXi5Gw==}
engines: {node: '>=20.0.0'}
peerDependencies:
react: '>=18'
@@ -3723,6 +4358,14 @@ packages:
resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==}
engines: {node: '>= 4'}
+ redis-errors@1.2.0:
+ resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==}
+ engines: {node: '>=4'}
+
+ redis-parser@3.0.0:
+ resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==}
+ engines: {node: '>=4'}
+
reflect.getprototypeof@1.0.10:
resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
engines: {node: '>= 0.4'}
@@ -3752,10 +4395,6 @@ packages:
resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==}
hasBin: true
- retry@0.12.0:
- resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==}
- engines: {node: '>= 4'}
-
reusify@1.1.0:
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
@@ -3765,6 +4404,9 @@ packages:
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
+ rou3@0.7.12:
+ resolution: {integrity: sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg==}
+
router@2.2.0:
resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==}
engines: {node: '>= 18'}
@@ -3818,6 +4460,11 @@ packages:
engines: {node: '>=10'}
hasBin: true
+ semver@7.7.4:
+ resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==}
+ engines: {node: '>=10'}
+ hasBin: true
+
send@0.19.0:
resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==}
engines: {node: '>= 0.8.0'}
@@ -3837,12 +4484,12 @@ packages:
resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==}
engines: {node: '>= 18'}
- set-cookie-parser@2.7.1:
- resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==}
-
set-cookie-parser@2.7.2:
resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==}
+ set-cookie-parser@3.1.0:
+ resolution: {integrity: sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==}
+
set-function-length@1.2.2:
resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
engines: {node: '>= 0.4'}
@@ -3914,17 +4561,12 @@ packages:
engines: {node: '>= 8'}
deprecated: The work that was done in this beta branch won't be included in future versions
- spdx-correct@3.2.0:
- resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==}
-
- spdx-exceptions@2.5.0:
- resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==}
+ split2@4.2.0:
+ resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
+ engines: {node: '>= 10.x'}
- spdx-expression-parse@3.0.1:
- resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==}
-
- spdx-license-ids@3.0.21:
- resolution: {integrity: sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==}
+ standard-as-callback@2.1.0:
+ resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==}
statuses@2.0.1:
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
@@ -3938,6 +4580,9 @@ packages:
resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==}
engines: {node: '>= 0.4'}
+ stream-browserify@3.0.0:
+ resolution: {integrity: sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==}
+
streamsearch@1.1.0:
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
engines: {node: '>=10.0.0'}
@@ -3972,6 +4617,9 @@ packages:
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
engines: {node: '>=8'}
+ strnum@2.2.3:
+ resolution: {integrity: sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==}
+
style-loader@4.0.0:
resolution: {integrity: sha512-1V4WqhhZZgjVAVJyt7TdDPZoPBPNHbekX4fWnCJL1yQukhCeZhJySUL+gL9y6sNdN95uEOS83Y55SqHcP7MzLA==}
engines: {node: '>= 18.12.0'}
@@ -4032,10 +4680,6 @@ packages:
tiny-invariant@1.3.3:
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
- tinyglobby@0.2.14:
- resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==}
- engines: {node: '>=12.0.0'}
-
tinyglobby@0.2.15:
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
engines: {node: '>=12.0.0'}
@@ -4139,12 +4783,6 @@ packages:
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
engines: {node: '>= 0.8'}
- update-browserslist-db@1.1.3:
- resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==}
- hasBin: true
- peerDependencies:
- browserslist: '>= 4.21.0'
-
update-browserslist-db@1.2.3:
resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
hasBin: true
@@ -4192,13 +4830,6 @@ packages:
typescript:
optional: true
- validate-npm-package-license@3.0.4:
- resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==}
-
- validate-npm-package-name@5.0.1:
- resolution: {integrity: sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==}
- engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
-
vary@1.1.2:
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
engines: {node: '>= 0.8'}
@@ -4307,11 +4938,6 @@ packages:
engines: {node: '>= 8'}
hasBin: true
- which@3.0.1:
- resolution: {integrity: sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==}
- engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
- hasBin: true
-
word-wrap@1.2.5:
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
engines: {node: '>=0.10.0'}
@@ -4356,8 +4982,8 @@ packages:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
- zod@4.0.9:
- resolution: {integrity: sha512-PAqUh1FW4Irxcs6/sWfnhJGV/8F0eLNd1FryydBfbA9Qvz1LaJk0TDrSC+hIKlArS/xVISuX/yjlS1bzg2D3Zw==}
+ zod@4.4.2:
+ resolution: {integrity: sha512-IynmDyxsEsb9RKzO3J9+4SxXnl2FTFSzNBaKKaMV6tsSk0rw9gYw9gs+JFCq/qk2LCZ78KDwyj+Z289TijSkUw==}
snapshots:
@@ -4366,6 +4992,483 @@ snapshots:
'@jridgewell/gen-mapping': 0.3.12
'@jridgewell/trace-mapping': 0.3.29
+ '@aws-crypto/crc32@5.2.0':
+ dependencies:
+ '@aws-crypto/util': 5.2.0
+ '@aws-sdk/types': 3.973.8
+ tslib: 2.8.1
+
+ '@aws-crypto/crc32c@5.2.0':
+ dependencies:
+ '@aws-crypto/util': 5.2.0
+ '@aws-sdk/types': 3.973.8
+ tslib: 2.8.1
+
+ '@aws-crypto/sha1-browser@5.2.0':
+ dependencies:
+ '@aws-crypto/supports-web-crypto': 5.2.0
+ '@aws-crypto/util': 5.2.0
+ '@aws-sdk/types': 3.973.8
+ '@aws-sdk/util-locate-window': 3.965.5
+ '@smithy/util-utf8': 2.3.0
+ tslib: 2.8.1
+
+ '@aws-crypto/sha256-browser@5.2.0':
+ dependencies:
+ '@aws-crypto/sha256-js': 5.2.0
+ '@aws-crypto/supports-web-crypto': 5.2.0
+ '@aws-crypto/util': 5.2.0
+ '@aws-sdk/types': 3.973.8
+ '@aws-sdk/util-locate-window': 3.965.5
+ '@smithy/util-utf8': 2.3.0
+ tslib: 2.8.1
+
+ '@aws-crypto/sha256-js@5.2.0':
+ dependencies:
+ '@aws-crypto/util': 5.2.0
+ '@aws-sdk/types': 3.973.8
+ tslib: 2.8.1
+
+ '@aws-crypto/supports-web-crypto@5.2.0':
+ dependencies:
+ tslib: 2.8.1
+
+ '@aws-crypto/util@5.2.0':
+ dependencies:
+ '@aws-sdk/types': 3.973.8
+ '@smithy/util-utf8': 2.3.0
+ tslib: 2.8.1
+
+ '@aws-sdk/client-s3@3.1041.0':
+ dependencies:
+ '@aws-crypto/sha1-browser': 5.2.0
+ '@aws-crypto/sha256-browser': 5.2.0
+ '@aws-crypto/sha256-js': 5.2.0
+ '@aws-sdk/core': 3.974.8
+ '@aws-sdk/credential-provider-node': 3.972.39
+ '@aws-sdk/middleware-bucket-endpoint': 3.972.10
+ '@aws-sdk/middleware-expect-continue': 3.972.10
+ '@aws-sdk/middleware-flexible-checksums': 3.974.16
+ '@aws-sdk/middleware-host-header': 3.972.10
+ '@aws-sdk/middleware-location-constraint': 3.972.10
+ '@aws-sdk/middleware-logger': 3.972.10
+ '@aws-sdk/middleware-recursion-detection': 3.972.11
+ '@aws-sdk/middleware-sdk-s3': 3.972.37
+ '@aws-sdk/middleware-ssec': 3.972.10
+ '@aws-sdk/middleware-user-agent': 3.972.38
+ '@aws-sdk/region-config-resolver': 3.972.13
+ '@aws-sdk/signature-v4-multi-region': 3.996.25
+ '@aws-sdk/types': 3.973.8
+ '@aws-sdk/util-endpoints': 3.996.8
+ '@aws-sdk/util-user-agent-browser': 3.972.10
+ '@aws-sdk/util-user-agent-node': 3.973.24
+ '@smithy/config-resolver': 4.4.17
+ '@smithy/core': 3.23.17
+ '@smithy/eventstream-serde-browser': 4.2.14
+ '@smithy/eventstream-serde-config-resolver': 4.3.14
+ '@smithy/eventstream-serde-node': 4.2.14
+ '@smithy/fetch-http-handler': 5.3.17
+ '@smithy/hash-blob-browser': 4.2.15
+ '@smithy/hash-node': 4.2.14
+ '@smithy/hash-stream-node': 4.2.14
+ '@smithy/invalid-dependency': 4.2.14
+ '@smithy/md5-js': 4.2.14
+ '@smithy/middleware-content-length': 4.2.14
+ '@smithy/middleware-endpoint': 4.4.32
+ '@smithy/middleware-retry': 4.5.7
+ '@smithy/middleware-serde': 4.2.20
+ '@smithy/middleware-stack': 4.2.14
+ '@smithy/node-config-provider': 4.3.14
+ '@smithy/node-http-handler': 4.6.1
+ '@smithy/protocol-http': 5.3.14
+ '@smithy/smithy-client': 4.12.13
+ '@smithy/types': 4.14.1
+ '@smithy/url-parser': 4.2.14
+ '@smithy/util-base64': 4.3.2
+ '@smithy/util-body-length-browser': 4.2.2
+ '@smithy/util-body-length-node': 4.2.3
+ '@smithy/util-defaults-mode-browser': 4.3.49
+ '@smithy/util-defaults-mode-node': 4.2.54
+ '@smithy/util-endpoints': 3.4.2
+ '@smithy/util-middleware': 4.2.14
+ '@smithy/util-retry': 4.3.8
+ '@smithy/util-stream': 4.5.25
+ '@smithy/util-utf8': 4.2.2
+ '@smithy/util-waiter': 4.3.0
+ tslib: 2.8.1
+ transitivePeerDependencies:
+ - aws-crt
+
+ '@aws-sdk/core@3.974.8':
+ dependencies:
+ '@aws-sdk/types': 3.973.8
+ '@aws-sdk/xml-builder': 3.972.22
+ '@smithy/core': 3.23.17
+ '@smithy/node-config-provider': 4.3.14
+ '@smithy/property-provider': 4.2.14
+ '@smithy/protocol-http': 5.3.14
+ '@smithy/signature-v4': 5.3.14
+ '@smithy/smithy-client': 4.12.13
+ '@smithy/types': 4.14.1
+ '@smithy/util-base64': 4.3.2
+ '@smithy/util-middleware': 4.2.14
+ '@smithy/util-retry': 4.3.8
+ '@smithy/util-utf8': 4.2.2
+ tslib: 2.8.1
+
+ '@aws-sdk/crc64-nvme@3.972.7':
+ dependencies:
+ '@smithy/types': 4.14.1
+ tslib: 2.8.1
+
+ '@aws-sdk/credential-provider-env@3.972.34':
+ dependencies:
+ '@aws-sdk/core': 3.974.8
+ '@aws-sdk/types': 3.973.8
+ '@smithy/property-provider': 4.2.14
+ '@smithy/types': 4.14.1
+ tslib: 2.8.1
+
+ '@aws-sdk/credential-provider-http@3.972.36':
+ dependencies:
+ '@aws-sdk/core': 3.974.8
+ '@aws-sdk/types': 3.973.8
+ '@smithy/fetch-http-handler': 5.3.17
+ '@smithy/node-http-handler': 4.6.1
+ '@smithy/property-provider': 4.2.14
+ '@smithy/protocol-http': 5.3.14
+ '@smithy/smithy-client': 4.12.13
+ '@smithy/types': 4.14.1
+ '@smithy/util-stream': 4.5.25
+ tslib: 2.8.1
+
+ '@aws-sdk/credential-provider-ini@3.972.38':
+ dependencies:
+ '@aws-sdk/core': 3.974.8
+ '@aws-sdk/credential-provider-env': 3.972.34
+ '@aws-sdk/credential-provider-http': 3.972.36
+ '@aws-sdk/credential-provider-login': 3.972.38
+ '@aws-sdk/credential-provider-process': 3.972.34
+ '@aws-sdk/credential-provider-sso': 3.972.38
+ '@aws-sdk/credential-provider-web-identity': 3.972.38
+ '@aws-sdk/nested-clients': 3.997.6
+ '@aws-sdk/types': 3.973.8
+ '@smithy/credential-provider-imds': 4.2.14
+ '@smithy/property-provider': 4.2.14
+ '@smithy/shared-ini-file-loader': 4.4.9
+ '@smithy/types': 4.14.1
+ tslib: 2.8.1
+ transitivePeerDependencies:
+ - aws-crt
+
+ '@aws-sdk/credential-provider-login@3.972.38':
+ dependencies:
+ '@aws-sdk/core': 3.974.8
+ '@aws-sdk/nested-clients': 3.997.6
+ '@aws-sdk/types': 3.973.8
+ '@smithy/property-provider': 4.2.14
+ '@smithy/protocol-http': 5.3.14
+ '@smithy/shared-ini-file-loader': 4.4.9
+ '@smithy/types': 4.14.1
+ tslib: 2.8.1
+ transitivePeerDependencies:
+ - aws-crt
+
+ '@aws-sdk/credential-provider-node@3.972.39':
+ dependencies:
+ '@aws-sdk/credential-provider-env': 3.972.34
+ '@aws-sdk/credential-provider-http': 3.972.36
+ '@aws-sdk/credential-provider-ini': 3.972.38
+ '@aws-sdk/credential-provider-process': 3.972.34
+ '@aws-sdk/credential-provider-sso': 3.972.38
+ '@aws-sdk/credential-provider-web-identity': 3.972.38
+ '@aws-sdk/types': 3.973.8
+ '@smithy/credential-provider-imds': 4.2.14
+ '@smithy/property-provider': 4.2.14
+ '@smithy/shared-ini-file-loader': 4.4.9
+ '@smithy/types': 4.14.1
+ tslib: 2.8.1
+ transitivePeerDependencies:
+ - aws-crt
+
+ '@aws-sdk/credential-provider-process@3.972.34':
+ dependencies:
+ '@aws-sdk/core': 3.974.8
+ '@aws-sdk/types': 3.973.8
+ '@smithy/property-provider': 4.2.14
+ '@smithy/shared-ini-file-loader': 4.4.9
+ '@smithy/types': 4.14.1
+ tslib: 2.8.1
+
+ '@aws-sdk/credential-provider-sso@3.972.38':
+ dependencies:
+ '@aws-sdk/core': 3.974.8
+ '@aws-sdk/nested-clients': 3.997.6
+ '@aws-sdk/token-providers': 3.1041.0
+ '@aws-sdk/types': 3.973.8
+ '@smithy/property-provider': 4.2.14
+ '@smithy/shared-ini-file-loader': 4.4.9
+ '@smithy/types': 4.14.1
+ tslib: 2.8.1
+ transitivePeerDependencies:
+ - aws-crt
+
+ '@aws-sdk/credential-provider-web-identity@3.972.38':
+ dependencies:
+ '@aws-sdk/core': 3.974.8
+ '@aws-sdk/nested-clients': 3.997.6
+ '@aws-sdk/types': 3.973.8
+ '@smithy/property-provider': 4.2.14
+ '@smithy/shared-ini-file-loader': 4.4.9
+ '@smithy/types': 4.14.1
+ tslib: 2.8.1
+ transitivePeerDependencies:
+ - aws-crt
+
+ '@aws-sdk/lib-storage@3.1041.0(@aws-sdk/client-s3@3.1041.0)':
+ dependencies:
+ '@aws-sdk/client-s3': 3.1041.0
+ '@smithy/middleware-endpoint': 4.4.32
+ '@smithy/protocol-http': 5.3.14
+ '@smithy/smithy-client': 4.12.13
+ '@smithy/types': 4.14.1
+ buffer: 5.6.0
+ events: 3.3.0
+ stream-browserify: 3.0.0
+ tslib: 2.8.1
+
+ '@aws-sdk/middleware-bucket-endpoint@3.972.10':
+ dependencies:
+ '@aws-sdk/types': 3.973.8
+ '@aws-sdk/util-arn-parser': 3.972.3
+ '@smithy/node-config-provider': 4.3.14
+ '@smithy/protocol-http': 5.3.14
+ '@smithy/types': 4.14.1
+ '@smithy/util-config-provider': 4.2.2
+ tslib: 2.8.1
+
+ '@aws-sdk/middleware-expect-continue@3.972.10':
+ dependencies:
+ '@aws-sdk/types': 3.973.8
+ '@smithy/protocol-http': 5.3.14
+ '@smithy/types': 4.14.1
+ tslib: 2.8.1
+
+ '@aws-sdk/middleware-flexible-checksums@3.974.16':
+ dependencies:
+ '@aws-crypto/crc32': 5.2.0
+ '@aws-crypto/crc32c': 5.2.0
+ '@aws-crypto/util': 5.2.0
+ '@aws-sdk/core': 3.974.8
+ '@aws-sdk/crc64-nvme': 3.972.7
+ '@aws-sdk/types': 3.973.8
+ '@smithy/is-array-buffer': 4.2.2
+ '@smithy/node-config-provider': 4.3.14
+ '@smithy/protocol-http': 5.3.14
+ '@smithy/types': 4.14.1
+ '@smithy/util-middleware': 4.2.14
+ '@smithy/util-stream': 4.5.25
+ '@smithy/util-utf8': 4.2.2
+ tslib: 2.8.1
+
+ '@aws-sdk/middleware-host-header@3.972.10':
+ dependencies:
+ '@aws-sdk/types': 3.973.8
+ '@smithy/protocol-http': 5.3.14
+ '@smithy/types': 4.14.1
+ tslib: 2.8.1
+
+ '@aws-sdk/middleware-location-constraint@3.972.10':
+ dependencies:
+ '@aws-sdk/types': 3.973.8
+ '@smithy/types': 4.14.1
+ tslib: 2.8.1
+
+ '@aws-sdk/middleware-logger@3.972.10':
+ dependencies:
+ '@aws-sdk/types': 3.973.8
+ '@smithy/types': 4.14.1
+ tslib: 2.8.1
+
+ '@aws-sdk/middleware-recursion-detection@3.972.11':
+ dependencies:
+ '@aws-sdk/types': 3.973.8
+ '@aws/lambda-invoke-store': 0.2.4
+ '@smithy/protocol-http': 5.3.14
+ '@smithy/types': 4.14.1
+ tslib: 2.8.1
+
+ '@aws-sdk/middleware-sdk-s3@3.972.37':
+ dependencies:
+ '@aws-sdk/core': 3.974.8
+ '@aws-sdk/types': 3.973.8
+ '@aws-sdk/util-arn-parser': 3.972.3
+ '@smithy/core': 3.23.17
+ '@smithy/node-config-provider': 4.3.14
+ '@smithy/protocol-http': 5.3.14
+ '@smithy/signature-v4': 5.3.14
+ '@smithy/smithy-client': 4.12.13
+ '@smithy/types': 4.14.1
+ '@smithy/util-config-provider': 4.2.2
+ '@smithy/util-middleware': 4.2.14
+ '@smithy/util-stream': 4.5.25
+ '@smithy/util-utf8': 4.2.2
+ tslib: 2.8.1
+
+ '@aws-sdk/middleware-ssec@3.972.10':
+ dependencies:
+ '@aws-sdk/types': 3.973.8
+ '@smithy/types': 4.14.1
+ tslib: 2.8.1
+
+ '@aws-sdk/middleware-user-agent@3.972.38':
+ dependencies:
+ '@aws-sdk/core': 3.974.8
+ '@aws-sdk/types': 3.973.8
+ '@aws-sdk/util-endpoints': 3.996.8
+ '@smithy/core': 3.23.17
+ '@smithy/protocol-http': 5.3.14
+ '@smithy/types': 4.14.1
+ '@smithy/util-retry': 4.3.8
+ tslib: 2.8.1
+
+ '@aws-sdk/nested-clients@3.997.6':
+ dependencies:
+ '@aws-crypto/sha256-browser': 5.2.0
+ '@aws-crypto/sha256-js': 5.2.0
+ '@aws-sdk/core': 3.974.8
+ '@aws-sdk/middleware-host-header': 3.972.10
+ '@aws-sdk/middleware-logger': 3.972.10
+ '@aws-sdk/middleware-recursion-detection': 3.972.11
+ '@aws-sdk/middleware-user-agent': 3.972.38
+ '@aws-sdk/region-config-resolver': 3.972.13
+ '@aws-sdk/signature-v4-multi-region': 3.996.25
+ '@aws-sdk/types': 3.973.8
+ '@aws-sdk/util-endpoints': 3.996.8
+ '@aws-sdk/util-user-agent-browser': 3.972.10
+ '@aws-sdk/util-user-agent-node': 3.973.24
+ '@smithy/config-resolver': 4.4.17
+ '@smithy/core': 3.23.17
+ '@smithy/fetch-http-handler': 5.3.17
+ '@smithy/hash-node': 4.2.14
+ '@smithy/invalid-dependency': 4.2.14
+ '@smithy/middleware-content-length': 4.2.14
+ '@smithy/middleware-endpoint': 4.4.32
+ '@smithy/middleware-retry': 4.5.7
+ '@smithy/middleware-serde': 4.2.20
+ '@smithy/middleware-stack': 4.2.14
+ '@smithy/node-config-provider': 4.3.14
+ '@smithy/node-http-handler': 4.6.1
+ '@smithy/protocol-http': 5.3.14
+ '@smithy/smithy-client': 4.12.13
+ '@smithy/types': 4.14.1
+ '@smithy/url-parser': 4.2.14
+ '@smithy/util-base64': 4.3.2
+ '@smithy/util-body-length-browser': 4.2.2
+ '@smithy/util-body-length-node': 4.2.3
+ '@smithy/util-defaults-mode-browser': 4.3.49
+ '@smithy/util-defaults-mode-node': 4.2.54
+ '@smithy/util-endpoints': 3.4.2
+ '@smithy/util-middleware': 4.2.14
+ '@smithy/util-retry': 4.3.8
+ '@smithy/util-utf8': 4.2.2
+ tslib: 2.8.1
+ transitivePeerDependencies:
+ - aws-crt
+
+ '@aws-sdk/region-config-resolver@3.972.13':
+ dependencies:
+ '@aws-sdk/types': 3.973.8
+ '@smithy/config-resolver': 4.4.17
+ '@smithy/node-config-provider': 4.3.14
+ '@smithy/types': 4.14.1
+ tslib: 2.8.1
+
+ '@aws-sdk/s3-request-presigner@3.1041.0':
+ dependencies:
+ '@aws-sdk/signature-v4-multi-region': 3.996.25
+ '@aws-sdk/types': 3.973.8
+ '@aws-sdk/util-format-url': 3.972.10
+ '@smithy/middleware-endpoint': 4.4.32
+ '@smithy/protocol-http': 5.3.14
+ '@smithy/smithy-client': 4.12.13
+ '@smithy/types': 4.14.1
+ tslib: 2.8.1
+
+ '@aws-sdk/signature-v4-multi-region@3.996.25':
+ dependencies:
+ '@aws-sdk/middleware-sdk-s3': 3.972.37
+ '@aws-sdk/types': 3.973.8
+ '@smithy/protocol-http': 5.3.14
+ '@smithy/signature-v4': 5.3.14
+ '@smithy/types': 4.14.1
+ tslib: 2.8.1
+
+ '@aws-sdk/token-providers@3.1041.0':
+ dependencies:
+ '@aws-sdk/core': 3.974.8
+ '@aws-sdk/nested-clients': 3.997.6
+ '@aws-sdk/types': 3.973.8
+ '@smithy/property-provider': 4.2.14
+ '@smithy/shared-ini-file-loader': 4.4.9
+ '@smithy/types': 4.14.1
+ tslib: 2.8.1
+ transitivePeerDependencies:
+ - aws-crt
+
+ '@aws-sdk/types@3.973.8':
+ dependencies:
+ '@smithy/types': 4.14.1
+ tslib: 2.8.1
+
+ '@aws-sdk/util-arn-parser@3.972.3':
+ dependencies:
+ tslib: 2.8.1
+
+ '@aws-sdk/util-endpoints@3.996.8':
+ dependencies:
+ '@aws-sdk/types': 3.973.8
+ '@smithy/types': 4.14.1
+ '@smithy/url-parser': 4.2.14
+ '@smithy/util-endpoints': 3.4.2
+ tslib: 2.8.1
+
+ '@aws-sdk/util-format-url@3.972.10':
+ dependencies:
+ '@aws-sdk/types': 3.973.8
+ '@smithy/querystring-builder': 4.2.14
+ '@smithy/types': 4.14.1
+ tslib: 2.8.1
+
+ '@aws-sdk/util-locate-window@3.965.5':
+ dependencies:
+ tslib: 2.8.1
+
+ '@aws-sdk/util-user-agent-browser@3.972.10':
+ dependencies:
+ '@aws-sdk/types': 3.973.8
+ '@smithy/types': 4.14.1
+ bowser: 2.14.1
+ tslib: 2.8.1
+
+ '@aws-sdk/util-user-agent-node@3.973.24':
+ dependencies:
+ '@aws-sdk/middleware-user-agent': 3.972.38
+ '@aws-sdk/types': 3.973.8
+ '@smithy/node-config-provider': 4.3.14
+ '@smithy/types': 4.14.1
+ '@smithy/util-config-provider': 4.2.2
+ tslib: 2.8.1
+
+ '@aws-sdk/xml-builder@3.972.22':
+ dependencies:
+ '@nodable/entities': 2.1.0
+ '@smithy/types': 4.14.1
+ fast-xml-parser: 5.7.2
+ tslib: 2.8.1
+
+ '@aws/lambda-invoke-store@0.2.4': {}
+
'@babel/code-frame@7.27.1':
dependencies:
'@babel/helper-validator-identifier': 7.27.1
@@ -4387,7 +5490,7 @@ snapshots:
'@babel/traverse': 7.28.0
'@babel/types': 7.28.2
convert-source-map: 2.0.0
- debug: 4.4.1
+ debug: 4.4.3
gensync: 1.0.0-beta.2
json5: 2.2.3
semver: 6.3.1
@@ -4410,7 +5513,7 @@ snapshots:
dependencies:
'@babel/compat-data': 7.28.0
'@babel/helper-validator-option': 7.27.1
- browserslist: 4.25.1
+ browserslist: 4.28.1
lru-cache: 5.1.1
semver: 6.3.1
@@ -4547,7 +5650,7 @@ snapshots:
'@babel/parser': 7.28.0
'@babel/template': 7.27.2
'@babel/types': 7.28.2
- debug: 4.4.1
+ debug: 4.4.3
transitivePeerDependencies:
- supports-color
@@ -4556,6 +5659,57 @@ snapshots:
'@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.27.1
+ '@better-auth/core@1.6.9(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.4.2))(jose@6.2.3)(kysely@0.28.16)(nanostores@1.3.0)':
+ dependencies:
+ '@better-auth/utils': 0.4.0
+ '@better-fetch/fetch': 1.1.21
+ '@opentelemetry/semantic-conventions': 1.40.0
+ '@standard-schema/spec': 1.1.0
+ better-call: 1.3.5(zod@4.4.2)
+ jose: 6.2.3
+ kysely: 0.28.16
+ nanostores: 1.3.0
+ zod: 4.4.2
+
+ '@better-auth/drizzle-adapter@1.6.9(@better-auth/core@1.6.9(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.4.2))(jose@6.2.3)(kysely@0.28.16)(nanostores@1.3.0))(@better-auth/utils@0.4.0)':
+ dependencies:
+ '@better-auth/core': 1.6.9(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.4.2))(jose@6.2.3)(kysely@0.28.16)(nanostores@1.3.0)
+ '@better-auth/utils': 0.4.0
+
+ '@better-auth/kysely-adapter@1.6.9(@better-auth/core@1.6.9(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.4.2))(jose@6.2.3)(kysely@0.28.16)(nanostores@1.3.0))(@better-auth/utils@0.4.0)(kysely@0.28.16)':
+ dependencies:
+ '@better-auth/core': 1.6.9(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.4.2))(jose@6.2.3)(kysely@0.28.16)(nanostores@1.3.0)
+ '@better-auth/utils': 0.4.0
+ optionalDependencies:
+ kysely: 0.28.16
+
+ '@better-auth/memory-adapter@1.6.9(@better-auth/core@1.6.9(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.4.2))(jose@6.2.3)(kysely@0.28.16)(nanostores@1.3.0))(@better-auth/utils@0.4.0)':
+ dependencies:
+ '@better-auth/core': 1.6.9(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.4.2))(jose@6.2.3)(kysely@0.28.16)(nanostores@1.3.0)
+ '@better-auth/utils': 0.4.0
+
+ '@better-auth/mongo-adapter@1.6.9(@better-auth/core@1.6.9(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.4.2))(jose@6.2.3)(kysely@0.28.16)(nanostores@1.3.0))(@better-auth/utils@0.4.0)':
+ dependencies:
+ '@better-auth/core': 1.6.9(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.4.2))(jose@6.2.3)(kysely@0.28.16)(nanostores@1.3.0)
+ '@better-auth/utils': 0.4.0
+
+ '@better-auth/prisma-adapter@1.6.9(@better-auth/core@1.6.9(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.4.2))(jose@6.2.3)(kysely@0.28.16)(nanostores@1.3.0))(@better-auth/utils@0.4.0)':
+ dependencies:
+ '@better-auth/core': 1.6.9(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.4.2))(jose@6.2.3)(kysely@0.28.16)(nanostores@1.3.0)
+ '@better-auth/utils': 0.4.0
+
+ '@better-auth/telemetry@1.6.9(@better-auth/core@1.6.9(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.4.2))(jose@6.2.3)(kysely@0.28.16)(nanostores@1.3.0))(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)':
+ dependencies:
+ '@better-auth/core': 1.6.9(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.4.2))(jose@6.2.3)(kysely@0.28.16)(nanostores@1.3.0)
+ '@better-auth/utils': 0.4.0
+ '@better-fetch/fetch': 1.1.21
+
+ '@better-auth/utils@0.4.0':
+ dependencies:
+ '@noble/hashes': 2.2.0
+
+ '@better-fetch/fetch@1.1.21': {}
+
'@cspotcode/source-map-support@0.8.1':
dependencies:
'@jridgewell/trace-mapping': 0.3.9
@@ -4865,6 +6019,8 @@ snapshots:
'@humanwhocodes/retry@0.4.3': {}
+ '@ioredis/commands@1.5.1': {}
+
'@isaacs/fs-minipass@4.0.1':
dependencies:
minipass: 7.1.2
@@ -4895,6 +6051,30 @@ snapshots:
'@mjackson/node-fetch-server@0.2.0': {}
+ '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3':
+ optional: true
+
+ '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3':
+ optional: true
+
+ '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3':
+ optional: true
+
+ '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3':
+ optional: true
+
+ '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3':
+ optional: true
+
+ '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3':
+ optional: true
+
+ '@noble/ciphers@2.2.0': {}
+
+ '@noble/hashes@2.2.0': {}
+
+ '@nodable/entities@2.1.0': {}
+
'@nodelib/fs.scandir@2.1.5':
dependencies:
'@nodelib/fs.stat': 2.0.5
@@ -4907,34 +6087,7 @@ snapshots:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.19.1
- '@npmcli/git@4.1.0':
- dependencies:
- '@npmcli/promise-spawn': 6.0.2
- lru-cache: 7.18.3
- npm-pick-manifest: 8.0.2
- proc-log: 3.0.0
- promise-inflight: 1.0.1
- promise-retry: 2.0.1
- semver: 7.7.2
- which: 3.0.1
- transitivePeerDependencies:
- - bluebird
-
- '@npmcli/package-json@4.0.1':
- dependencies:
- '@npmcli/git': 4.1.0
- glob: 13.0.0
- hosted-git-info: 6.1.3
- json-parse-even-better-errors: 3.0.2
- normalize-package-data: 5.0.0
- proc-log: 3.0.0
- semver: 7.7.2
- transitivePeerDependencies:
- - bluebird
-
- '@npmcli/promise-spawn@6.0.2':
- dependencies:
- which: 3.0.1
+ '@opentelemetry/semantic-conventions@1.40.0': {}
'@radix-ui/number@1.1.1': {}
@@ -5421,12 +6574,7 @@ snapshots:
'@radix-ui/rect@1.1.1': {}
- '@react-oauth/google@0.13.4(react-dom@19.1.1(react@19.1.1))(react@19.1.1)':
- dependencies:
- react: 19.1.1
- react-dom: 19.1.1(react@19.1.1)
-
- '@react-router/dev@7.7.1(@react-router/serve@7.7.1(react-router@7.12.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(typescript@5.8.3))(@types/node@24.1.0)(jiti@2.5.1)(lightningcss@1.30.1)(react-router@7.12.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(terser@5.43.1)(tsx@4.20.4)(typescript@5.8.3)(vite@7.3.1(@types/node@24.1.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4))':
+ '@react-router/dev@7.14.2(@react-router/serve@7.14.2(react-router@7.14.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(typescript@5.8.3))(@types/node@24.1.0)(jiti@2.5.1)(lightningcss@1.30.1)(react-router@7.14.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(terser@5.43.1)(tsx@4.20.4)(typescript@5.8.3)(vite@7.3.1(@types/node@24.1.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4))':
dependencies:
'@babel/core': 7.28.0
'@babel/generator': 7.28.0
@@ -5435,8 +6583,8 @@ snapshots:
'@babel/preset-typescript': 7.27.1(@babel/core@7.28.0)
'@babel/traverse': 7.28.0
'@babel/types': 7.28.2
- '@npmcli/package-json': 4.0.1
- '@react-router/node': 7.12.0(react-router@7.12.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(typescript@5.8.3)
+ '@react-router/node': 7.14.2(react-router@7.14.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(typescript@5.8.3)
+ '@remix-run/node-fetch-server': 0.13.1
arg: 5.0.2
babel-dead-code-elimination: 1.0.10
chokidar: 4.0.3
@@ -5446,24 +6594,24 @@ snapshots:
isbot: 5.1.29
jsesc: 3.0.2
lodash: 4.17.23
+ p-map: 7.0.4
pathe: 1.1.2
picocolors: 1.1.1
+ pkg-types: 2.3.1
prettier: 3.6.2
react-refresh: 0.14.2
- react-router: 7.12.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
+ react-router: 7.14.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
semver: 7.7.2
- set-cookie-parser: 2.7.1
- tinyglobby: 0.2.14
+ tinyglobby: 0.2.15
valibot: 1.2.0(typescript@5.8.3)
vite: 7.3.1(@types/node@24.1.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)
vite-node: 3.2.4(@types/node@24.1.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)
optionalDependencies:
- '@react-router/serve': 7.7.1(react-router@7.12.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(typescript@5.8.3)
+ '@react-router/serve': 7.14.2(react-router@7.14.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(typescript@5.8.3)
typescript: 5.8.3
transitivePeerDependencies:
- '@types/node'
- babel-plugin-macros
- - bluebird
- jiti
- less
- lightningcss
@@ -5476,35 +6624,38 @@ snapshots:
- tsx
- yaml
- '@react-router/express@7.7.1(express@4.21.2)(react-router@7.12.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(typescript@5.8.3)':
+ '@react-router/express@7.14.2(express@4.21.2)(react-router@7.14.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(typescript@5.8.3)':
dependencies:
- '@react-router/node': 7.12.0(react-router@7.12.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(typescript@5.8.3)
+ '@react-router/node': 7.14.2(react-router@7.14.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(typescript@5.8.3)
express: 4.21.2
- react-router: 7.12.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
+ react-router: 7.14.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
optionalDependencies:
typescript: 5.8.3
- '@react-router/node@7.12.0(react-router@7.12.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(typescript@5.8.3)':
+ '@react-router/node@7.14.2(react-router@7.14.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(typescript@5.8.3)':
dependencies:
'@mjackson/node-fetch-server': 0.2.0
- react-router: 7.12.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
+ react-router: 7.14.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
optionalDependencies:
typescript: 5.8.3
- '@react-router/serve@7.7.1(react-router@7.12.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(typescript@5.8.3)':
+ '@react-router/serve@7.14.2(react-router@7.14.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(typescript@5.8.3)':
dependencies:
- '@react-router/express': 7.7.1(express@4.21.2)(react-router@7.12.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(typescript@5.8.3)
- '@react-router/node': 7.12.0(react-router@7.12.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(typescript@5.8.3)
+ '@mjackson/node-fetch-server': 0.2.0
+ '@react-router/express': 7.14.2(express@4.21.2)(react-router@7.14.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(typescript@5.8.3)
+ '@react-router/node': 7.14.2(react-router@7.14.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(typescript@5.8.3)
compression: 1.8.1
express: 4.21.2
get-port: 5.1.1
morgan: 1.10.1
- react-router: 7.12.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
+ react-router: 7.14.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
source-map-support: 0.5.21
transitivePeerDependencies:
- supports-color
- typescript
+ '@remix-run/node-fetch-server@0.13.1': {}
+
'@remotion/bundler@4.0.329(react-dom@19.1.1(react@19.1.1))(react@19.1.1)':
dependencies:
'@remotion/media-parser': 4.0.329
@@ -5661,109 +6812,444 @@ snapshots:
'@remotion/studio@4.0.329(react-dom@19.1.1(react@19.1.1))(react@19.1.1)':
dependencies:
- '@remotion/media-parser': 4.0.329
- '@remotion/media-utils': 4.0.329(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
- '@remotion/player': 4.0.329(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
- '@remotion/renderer': 4.0.329(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
- '@remotion/studio-shared': 4.0.329(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
- '@remotion/webcodecs': 4.0.329
- '@remotion/zod-types': 4.0.329(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(zod@4.0.9)
- memfs: 3.4.3
- open: 8.4.2
- react: 19.1.1
- react-dom: 19.1.1(react@19.1.1)
- remotion: 4.0.329(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
- semver: 7.5.3
- source-map: 0.7.3
- zod: 4.0.9
- transitivePeerDependencies:
- - bufferutil
- - supports-color
- - utf-8-validate
+ '@remotion/media-parser': 4.0.329
+ '@remotion/media-utils': 4.0.329(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
+ '@remotion/player': 4.0.329(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
+ '@remotion/renderer': 4.0.329(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
+ '@remotion/studio-shared': 4.0.329(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
+ '@remotion/webcodecs': 4.0.329
+ '@remotion/zod-types': 4.0.329(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(zod@4.4.2)
+ memfs: 3.4.3
+ open: 8.4.2
+ react: 19.1.1
+ react-dom: 19.1.1(react@19.1.1)
+ remotion: 4.0.329(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
+ semver: 7.5.3
+ source-map: 0.7.3
+ zod: 4.4.2
+ transitivePeerDependencies:
+ - bufferutil
+ - supports-color
+ - utf-8-validate
+
+ '@remotion/transitions@4.0.329(react-dom@19.1.1(react@19.1.1))(react@19.1.1)':
+ dependencies:
+ '@remotion/paths': 4.0.329
+ '@remotion/shapes': 4.0.329(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
+ react: 19.1.1
+ react-dom: 19.1.1(react@19.1.1)
+ remotion: 4.0.329(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
+
+ '@remotion/webcodecs@4.0.329':
+ dependencies:
+ '@remotion/licensing': 4.0.329
+ '@remotion/media-parser': 4.0.329
+
+ '@remotion/zod-types@4.0.329(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(zod@4.4.2)':
+ dependencies:
+ remotion: 4.0.329(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
+ zod: 4.4.2
+ transitivePeerDependencies:
+ - react
+ - react-dom
+
+ '@rollup/rollup-android-arm-eabi@4.50.1':
+ optional: true
+
+ '@rollup/rollup-android-arm64@4.50.1':
+ optional: true
+
+ '@rollup/rollup-darwin-arm64@4.50.1':
+ optional: true
+
+ '@rollup/rollup-darwin-x64@4.50.1':
+ optional: true
+
+ '@rollup/rollup-freebsd-arm64@4.50.1':
+ optional: true
+
+ '@rollup/rollup-freebsd-x64@4.50.1':
+ optional: true
+
+ '@rollup/rollup-linux-arm-gnueabihf@4.50.1':
+ optional: true
+
+ '@rollup/rollup-linux-arm-musleabihf@4.50.1':
+ optional: true
+
+ '@rollup/rollup-linux-arm64-gnu@4.50.1':
+ optional: true
+
+ '@rollup/rollup-linux-arm64-musl@4.50.1':
+ optional: true
+
+ '@rollup/rollup-linux-loongarch64-gnu@4.50.1':
+ optional: true
+
+ '@rollup/rollup-linux-ppc64-gnu@4.50.1':
+ optional: true
+
+ '@rollup/rollup-linux-riscv64-gnu@4.50.1':
+ optional: true
+
+ '@rollup/rollup-linux-riscv64-musl@4.50.1':
+ optional: true
+
+ '@rollup/rollup-linux-s390x-gnu@4.50.1':
+ optional: true
+
+ '@rollup/rollup-linux-x64-gnu@4.50.1':
+ optional: true
+
+ '@rollup/rollup-linux-x64-musl@4.50.1':
+ optional: true
+
+ '@rollup/rollup-openharmony-arm64@4.50.1':
+ optional: true
+
+ '@rollup/rollup-win32-arm64-msvc@4.50.1':
+ optional: true
+
+ '@rollup/rollup-win32-ia32-msvc@4.50.1':
+ optional: true
+
+ '@rollup/rollup-win32-x64-msvc@4.50.1':
+ optional: true
+
+ '@smithy/chunked-blob-reader-native@4.2.3':
+ dependencies:
+ '@smithy/util-base64': 4.3.2
+ tslib: 2.8.1
+
+ '@smithy/chunked-blob-reader@5.2.2':
+ dependencies:
+ tslib: 2.8.1
+
+ '@smithy/config-resolver@4.4.17':
+ dependencies:
+ '@smithy/node-config-provider': 4.3.14
+ '@smithy/types': 4.14.1
+ '@smithy/util-config-provider': 4.2.2
+ '@smithy/util-endpoints': 3.4.2
+ '@smithy/util-middleware': 4.2.14
+ tslib: 2.8.1
+
+ '@smithy/core@3.23.17':
+ dependencies:
+ '@smithy/protocol-http': 5.3.14
+ '@smithy/types': 4.14.1
+ '@smithy/url-parser': 4.2.14
+ '@smithy/util-base64': 4.3.2
+ '@smithy/util-body-length-browser': 4.2.2
+ '@smithy/util-middleware': 4.2.14
+ '@smithy/util-stream': 4.5.25
+ '@smithy/util-utf8': 4.2.2
+ '@smithy/uuid': 1.1.2
+ tslib: 2.8.1
+
+ '@smithy/credential-provider-imds@4.2.14':
+ dependencies:
+ '@smithy/node-config-provider': 4.3.14
+ '@smithy/property-provider': 4.2.14
+ '@smithy/types': 4.14.1
+ '@smithy/url-parser': 4.2.14
+ tslib: 2.8.1
+
+ '@smithy/eventstream-codec@4.2.14':
+ dependencies:
+ '@aws-crypto/crc32': 5.2.0
+ '@smithy/types': 4.14.1
+ '@smithy/util-hex-encoding': 4.2.2
+ tslib: 2.8.1
+
+ '@smithy/eventstream-serde-browser@4.2.14':
+ dependencies:
+ '@smithy/eventstream-serde-universal': 4.2.14
+ '@smithy/types': 4.14.1
+ tslib: 2.8.1
+
+ '@smithy/eventstream-serde-config-resolver@4.3.14':
+ dependencies:
+ '@smithy/types': 4.14.1
+ tslib: 2.8.1
+
+ '@smithy/eventstream-serde-node@4.2.14':
+ dependencies:
+ '@smithy/eventstream-serde-universal': 4.2.14
+ '@smithy/types': 4.14.1
+ tslib: 2.8.1
+
+ '@smithy/eventstream-serde-universal@4.2.14':
+ dependencies:
+ '@smithy/eventstream-codec': 4.2.14
+ '@smithy/types': 4.14.1
+ tslib: 2.8.1
+
+ '@smithy/fetch-http-handler@5.3.17':
+ dependencies:
+ '@smithy/protocol-http': 5.3.14
+ '@smithy/querystring-builder': 4.2.14
+ '@smithy/types': 4.14.1
+ '@smithy/util-base64': 4.3.2
+ tslib: 2.8.1
+
+ '@smithy/hash-blob-browser@4.2.15':
+ dependencies:
+ '@smithy/chunked-blob-reader': 5.2.2
+ '@smithy/chunked-blob-reader-native': 4.2.3
+ '@smithy/types': 4.14.1
+ tslib: 2.8.1
+
+ '@smithy/hash-node@4.2.14':
+ dependencies:
+ '@smithy/types': 4.14.1
+ '@smithy/util-buffer-from': 4.2.2
+ '@smithy/util-utf8': 4.2.2
+ tslib: 2.8.1
+
+ '@smithy/hash-stream-node@4.2.14':
+ dependencies:
+ '@smithy/types': 4.14.1
+ '@smithy/util-utf8': 4.2.2
+ tslib: 2.8.1
+
+ '@smithy/invalid-dependency@4.2.14':
+ dependencies:
+ '@smithy/types': 4.14.1
+ tslib: 2.8.1
+
+ '@smithy/is-array-buffer@2.2.0':
+ dependencies:
+ tslib: 2.8.1
+
+ '@smithy/is-array-buffer@4.2.2':
+ dependencies:
+ tslib: 2.8.1
+
+ '@smithy/md5-js@4.2.14':
+ dependencies:
+ '@smithy/types': 4.14.1
+ '@smithy/util-utf8': 4.2.2
+ tslib: 2.8.1
+
+ '@smithy/middleware-content-length@4.2.14':
+ dependencies:
+ '@smithy/protocol-http': 5.3.14
+ '@smithy/types': 4.14.1
+ tslib: 2.8.1
+
+ '@smithy/middleware-endpoint@4.4.32':
+ dependencies:
+ '@smithy/core': 3.23.17
+ '@smithy/middleware-serde': 4.2.20
+ '@smithy/node-config-provider': 4.3.14
+ '@smithy/shared-ini-file-loader': 4.4.9
+ '@smithy/types': 4.14.1
+ '@smithy/url-parser': 4.2.14
+ '@smithy/util-middleware': 4.2.14
+ tslib: 2.8.1
+
+ '@smithy/middleware-retry@4.5.7':
+ dependencies:
+ '@smithy/core': 3.23.17
+ '@smithy/node-config-provider': 4.3.14
+ '@smithy/protocol-http': 5.3.14
+ '@smithy/service-error-classification': 4.3.1
+ '@smithy/smithy-client': 4.12.13
+ '@smithy/types': 4.14.1
+ '@smithy/util-middleware': 4.2.14
+ '@smithy/util-retry': 4.3.8
+ '@smithy/uuid': 1.1.2
+ tslib: 2.8.1
+
+ '@smithy/middleware-serde@4.2.20':
+ dependencies:
+ '@smithy/core': 3.23.17
+ '@smithy/protocol-http': 5.3.14
+ '@smithy/types': 4.14.1
+ tslib: 2.8.1
+
+ '@smithy/middleware-stack@4.2.14':
+ dependencies:
+ '@smithy/types': 4.14.1
+ tslib: 2.8.1
+
+ '@smithy/node-config-provider@4.3.14':
+ dependencies:
+ '@smithy/property-provider': 4.2.14
+ '@smithy/shared-ini-file-loader': 4.4.9
+ '@smithy/types': 4.14.1
+ tslib: 2.8.1
+
+ '@smithy/node-http-handler@4.6.1':
+ dependencies:
+ '@smithy/protocol-http': 5.3.14
+ '@smithy/querystring-builder': 4.2.14
+ '@smithy/types': 4.14.1
+ tslib: 2.8.1
+
+ '@smithy/property-provider@4.2.14':
+ dependencies:
+ '@smithy/types': 4.14.1
+ tslib: 2.8.1
+
+ '@smithy/protocol-http@5.3.14':
+ dependencies:
+ '@smithy/types': 4.14.1
+ tslib: 2.8.1
+
+ '@smithy/querystring-builder@4.2.14':
+ dependencies:
+ '@smithy/types': 4.14.1
+ '@smithy/util-uri-escape': 4.2.2
+ tslib: 2.8.1
+
+ '@smithy/querystring-parser@4.2.14':
+ dependencies:
+ '@smithy/types': 4.14.1
+ tslib: 2.8.1
+
+ '@smithy/service-error-classification@4.3.1':
+ dependencies:
+ '@smithy/types': 4.14.1
- '@remotion/transitions@4.0.329(react-dom@19.1.1(react@19.1.1))(react@19.1.1)':
+ '@smithy/shared-ini-file-loader@4.4.9':
dependencies:
- '@remotion/paths': 4.0.329
- '@remotion/shapes': 4.0.329(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
- react: 19.1.1
- react-dom: 19.1.1(react@19.1.1)
- remotion: 4.0.329(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
+ '@smithy/types': 4.14.1
+ tslib: 2.8.1
- '@remotion/webcodecs@4.0.329':
+ '@smithy/signature-v4@5.3.14':
dependencies:
- '@remotion/licensing': 4.0.329
- '@remotion/media-parser': 4.0.329
+ '@smithy/is-array-buffer': 4.2.2
+ '@smithy/protocol-http': 5.3.14
+ '@smithy/types': 4.14.1
+ '@smithy/util-hex-encoding': 4.2.2
+ '@smithy/util-middleware': 4.2.14
+ '@smithy/util-uri-escape': 4.2.2
+ '@smithy/util-utf8': 4.2.2
+ tslib: 2.8.1
- '@remotion/zod-types@4.0.329(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(zod@4.0.9)':
+ '@smithy/smithy-client@4.12.13':
dependencies:
- remotion: 4.0.329(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
- zod: 4.0.9
- transitivePeerDependencies:
- - react
- - react-dom
+ '@smithy/core': 3.23.17
+ '@smithy/middleware-endpoint': 4.4.32
+ '@smithy/middleware-stack': 4.2.14
+ '@smithy/protocol-http': 5.3.14
+ '@smithy/types': 4.14.1
+ '@smithy/util-stream': 4.5.25
+ tslib: 2.8.1
- '@rollup/rollup-android-arm-eabi@4.50.1':
- optional: true
+ '@smithy/types@4.14.1':
+ dependencies:
+ tslib: 2.8.1
- '@rollup/rollup-android-arm64@4.50.1':
- optional: true
+ '@smithy/url-parser@4.2.14':
+ dependencies:
+ '@smithy/querystring-parser': 4.2.14
+ '@smithy/types': 4.14.1
+ tslib: 2.8.1
- '@rollup/rollup-darwin-arm64@4.50.1':
- optional: true
+ '@smithy/util-base64@4.3.2':
+ dependencies:
+ '@smithy/util-buffer-from': 4.2.2
+ '@smithy/util-utf8': 4.2.2
+ tslib: 2.8.1
- '@rollup/rollup-darwin-x64@4.50.1':
- optional: true
+ '@smithy/util-body-length-browser@4.2.2':
+ dependencies:
+ tslib: 2.8.1
- '@rollup/rollup-freebsd-arm64@4.50.1':
- optional: true
+ '@smithy/util-body-length-node@4.2.3':
+ dependencies:
+ tslib: 2.8.1
- '@rollup/rollup-freebsd-x64@4.50.1':
- optional: true
+ '@smithy/util-buffer-from@2.2.0':
+ dependencies:
+ '@smithy/is-array-buffer': 2.2.0
+ tslib: 2.8.1
- '@rollup/rollup-linux-arm-gnueabihf@4.50.1':
- optional: true
+ '@smithy/util-buffer-from@4.2.2':
+ dependencies:
+ '@smithy/is-array-buffer': 4.2.2
+ tslib: 2.8.1
- '@rollup/rollup-linux-arm-musleabihf@4.50.1':
- optional: true
+ '@smithy/util-config-provider@4.2.2':
+ dependencies:
+ tslib: 2.8.1
- '@rollup/rollup-linux-arm64-gnu@4.50.1':
- optional: true
+ '@smithy/util-defaults-mode-browser@4.3.49':
+ dependencies:
+ '@smithy/property-provider': 4.2.14
+ '@smithy/smithy-client': 4.12.13
+ '@smithy/types': 4.14.1
+ tslib: 2.8.1
- '@rollup/rollup-linux-arm64-musl@4.50.1':
- optional: true
+ '@smithy/util-defaults-mode-node@4.2.54':
+ dependencies:
+ '@smithy/config-resolver': 4.4.17
+ '@smithy/credential-provider-imds': 4.2.14
+ '@smithy/node-config-provider': 4.3.14
+ '@smithy/property-provider': 4.2.14
+ '@smithy/smithy-client': 4.12.13
+ '@smithy/types': 4.14.1
+ tslib: 2.8.1
- '@rollup/rollup-linux-loongarch64-gnu@4.50.1':
- optional: true
+ '@smithy/util-endpoints@3.4.2':
+ dependencies:
+ '@smithy/node-config-provider': 4.3.14
+ '@smithy/types': 4.14.1
+ tslib: 2.8.1
- '@rollup/rollup-linux-ppc64-gnu@4.50.1':
- optional: true
+ '@smithy/util-hex-encoding@4.2.2':
+ dependencies:
+ tslib: 2.8.1
- '@rollup/rollup-linux-riscv64-gnu@4.50.1':
- optional: true
+ '@smithy/util-middleware@4.2.14':
+ dependencies:
+ '@smithy/types': 4.14.1
+ tslib: 2.8.1
- '@rollup/rollup-linux-riscv64-musl@4.50.1':
- optional: true
+ '@smithy/util-retry@4.3.8':
+ dependencies:
+ '@smithy/service-error-classification': 4.3.1
+ '@smithy/types': 4.14.1
+ tslib: 2.8.1
- '@rollup/rollup-linux-s390x-gnu@4.50.1':
- optional: true
+ '@smithy/util-stream@4.5.25':
+ dependencies:
+ '@smithy/fetch-http-handler': 5.3.17
+ '@smithy/node-http-handler': 4.6.1
+ '@smithy/types': 4.14.1
+ '@smithy/util-base64': 4.3.2
+ '@smithy/util-buffer-from': 4.2.2
+ '@smithy/util-hex-encoding': 4.2.2
+ '@smithy/util-utf8': 4.2.2
+ tslib: 2.8.1
- '@rollup/rollup-linux-x64-gnu@4.50.1':
- optional: true
+ '@smithy/util-uri-escape@4.2.2':
+ dependencies:
+ tslib: 2.8.1
- '@rollup/rollup-linux-x64-musl@4.50.1':
- optional: true
+ '@smithy/util-utf8@2.3.0':
+ dependencies:
+ '@smithy/util-buffer-from': 2.2.0
+ tslib: 2.8.1
- '@rollup/rollup-openharmony-arm64@4.50.1':
- optional: true
+ '@smithy/util-utf8@4.2.2':
+ dependencies:
+ '@smithy/util-buffer-from': 4.2.2
+ tslib: 2.8.1
- '@rollup/rollup-win32-arm64-msvc@4.50.1':
- optional: true
+ '@smithy/util-waiter@4.3.0':
+ dependencies:
+ '@smithy/types': 4.14.1
+ tslib: 2.8.1
- '@rollup/rollup-win32-ia32-msvc@4.50.1':
- optional: true
+ '@smithy/uuid@1.1.2':
+ dependencies:
+ tslib: 2.8.1
- '@rollup/rollup-win32-x64-msvc@4.50.1':
- optional: true
+ '@standard-schema/spec@1.1.0': {}
'@tailwindcss/node@4.1.11':
dependencies:
@@ -5963,7 +7449,7 @@ snapshots:
dependencies:
'@typescript-eslint/tsconfig-utils': 8.38.0(typescript@5.8.3)
'@typescript-eslint/types': 8.38.0
- debug: 4.4.1
+ debug: 4.4.3
typescript: 5.8.3
transitivePeerDependencies:
- supports-color
@@ -6260,12 +7746,50 @@ snapshots:
balanced-match@4.0.3: {}
+ base64-js@1.5.1: {}
+
baseline-browser-mapping@2.9.19: {}
basic-auth@2.0.1:
dependencies:
safe-buffer: 5.1.2
+ better-auth@1.6.9(pg@8.20.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1):
+ dependencies:
+ '@better-auth/core': 1.6.9(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.4.2))(jose@6.2.3)(kysely@0.28.16)(nanostores@1.3.0)
+ '@better-auth/drizzle-adapter': 1.6.9(@better-auth/core@1.6.9(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.4.2))(jose@6.2.3)(kysely@0.28.16)(nanostores@1.3.0))(@better-auth/utils@0.4.0)
+ '@better-auth/kysely-adapter': 1.6.9(@better-auth/core@1.6.9(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.4.2))(jose@6.2.3)(kysely@0.28.16)(nanostores@1.3.0))(@better-auth/utils@0.4.0)(kysely@0.28.16)
+ '@better-auth/memory-adapter': 1.6.9(@better-auth/core@1.6.9(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.4.2))(jose@6.2.3)(kysely@0.28.16)(nanostores@1.3.0))(@better-auth/utils@0.4.0)
+ '@better-auth/mongo-adapter': 1.6.9(@better-auth/core@1.6.9(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.4.2))(jose@6.2.3)(kysely@0.28.16)(nanostores@1.3.0))(@better-auth/utils@0.4.0)
+ '@better-auth/prisma-adapter': 1.6.9(@better-auth/core@1.6.9(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.4.2))(jose@6.2.3)(kysely@0.28.16)(nanostores@1.3.0))(@better-auth/utils@0.4.0)
+ '@better-auth/telemetry': 1.6.9(@better-auth/core@1.6.9(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.4.2))(jose@6.2.3)(kysely@0.28.16)(nanostores@1.3.0))(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)
+ '@better-auth/utils': 0.4.0
+ '@better-fetch/fetch': 1.1.21
+ '@noble/ciphers': 2.2.0
+ '@noble/hashes': 2.2.0
+ better-call: 1.3.5(zod@4.4.2)
+ defu: 6.1.7
+ jose: 6.2.3
+ kysely: 0.28.16
+ nanostores: 1.3.0
+ zod: 4.4.2
+ optionalDependencies:
+ pg: 8.20.0
+ react: 19.1.1
+ react-dom: 19.1.1(react@19.1.1)
+ transitivePeerDependencies:
+ - '@cloudflare/workers-types'
+ - '@opentelemetry/api'
+
+ better-call@1.3.5(zod@4.4.2):
+ dependencies:
+ '@better-auth/utils': 0.4.0
+ '@better-fetch/fetch': 1.1.21
+ rou3: 0.7.12
+ set-cookie-parser: 3.1.0
+ optionalDependencies:
+ zod: 4.4.2
+
big.js@5.2.2: {}
body-parser@1.20.3:
@@ -6299,6 +7823,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ bowser@2.14.1: {}
+
brace-expansion@5.0.2:
dependencies:
balanced-match: 4.0.3
@@ -6307,13 +7833,6 @@ snapshots:
dependencies:
fill-range: 7.1.1
- browserslist@4.25.1:
- dependencies:
- caniuse-lite: 1.0.30001731
- electron-to-chromium: 1.5.192
- node-releases: 2.0.19
- update-browserslist-db: 1.1.3(browserslist@4.25.1)
-
browserslist@4.28.1:
dependencies:
baseline-browser-mapping: 2.9.19
@@ -6326,6 +7845,22 @@ snapshots:
buffer-from@1.1.2: {}
+ buffer@5.6.0:
+ dependencies:
+ base64-js: 1.5.1
+ ieee754: 1.2.1
+
+ bullmq@5.76.5:
+ dependencies:
+ cron-parser: 4.9.0
+ ioredis: 5.10.1
+ msgpackr: 1.11.12
+ node-abort-controller: 3.1.1
+ semver: 7.7.4
+ tslib: 2.8.1
+ transitivePeerDependencies:
+ - supports-color
+
busboy@1.6.0:
dependencies:
streamsearch: 1.1.0
@@ -6353,8 +7888,6 @@ snapshots:
callsites@3.1.0: {}
- caniuse-lite@1.0.30001731: {}
-
caniuse-lite@1.0.30001769: {}
chalk@4.1.2:
@@ -6376,6 +7909,8 @@ snapshots:
clsx@2.1.1: {}
+ cluster-key-slot@1.1.2: {}
+
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
@@ -6411,6 +7946,8 @@ snapshots:
readable-stream: 3.6.2
typedarray: 0.0.6
+ confbox@0.2.4: {}
+
content-disposition@0.5.4:
dependencies:
safe-buffer: 5.2.1
@@ -6440,6 +7977,10 @@ snapshots:
create-require@1.1.1: {}
+ cron-parser@4.9.0:
+ dependencies:
+ luxon: 3.7.2
+
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
@@ -6512,8 +8053,12 @@ snapshots:
has-property-descriptors: 1.0.2
object-keys: 1.1.1
+ defu@6.1.7: {}
+
delayed-stream@1.0.0: {}
+ denque@2.1.0: {}
+
depd@2.0.0: {}
destroy@1.2.0: {}
@@ -6540,8 +8085,6 @@ snapshots:
ee-first@1.1.1: {}
- electron-to-chromium@1.5.192: {}
-
electron-to-chromium@1.5.286: {}
emojis-list@3.0.0: {}
@@ -6564,8 +8107,6 @@ snapshots:
graceful-fs: 4.2.11
tapable: 2.3.0
- err-code@2.0.3: {}
-
es-abstract@1.24.0:
dependencies:
array-buffer-byte-length: 1.0.2
@@ -6953,6 +8494,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ exsolve@1.0.8: {}
+
extract-zip@2.0.1:
dependencies:
debug: 4.4.1
@@ -6979,6 +8522,17 @@ snapshots:
fast-uri@3.0.6: {}
+ fast-xml-builder@1.1.5:
+ dependencies:
+ path-expression-matcher: 1.5.0
+
+ fast-xml-parser@5.7.2:
+ dependencies:
+ '@nodable/entities': 2.1.0
+ fast-xml-builder: 1.1.5
+ path-expression-matcher: 1.5.0
+ strnum: 2.2.3
+
fastq@1.19.1:
dependencies:
reusify: 1.1.0
@@ -7013,7 +8567,7 @@ snapshots:
finalhandler@2.1.0:
dependencies:
- debug: 4.4.1
+ debug: 4.4.3
encodeurl: 2.0.0
escape-html: 1.0.3
on-finished: 2.4.1
@@ -7131,12 +8685,6 @@ snapshots:
glob-to-regexp@0.4.1: {}
- glob@13.0.0:
- dependencies:
- minimatch: 10.2.2
- minipass: 7.1.2
- path-scurry: 2.0.1
-
globals@14.0.0: {}
globalthis@1.0.4:
@@ -7174,10 +8722,6 @@ snapshots:
dependencies:
function-bind: 1.1.2
- hosted-git-info@6.1.3:
- dependencies:
- lru-cache: 7.18.3
-
http-errors@2.0.0:
dependencies:
depd: 2.0.0
@@ -7208,6 +8752,8 @@ snapshots:
dependencies:
postcss: 8.5.6
+ ieee754@1.2.1: {}
+
ignore@5.3.2: {}
ignore@7.0.5: {}
@@ -7227,6 +8773,20 @@ snapshots:
hasown: 2.0.2
side-channel: 1.1.0
+ ioredis@5.10.1:
+ dependencies:
+ '@ioredis/commands': 1.5.1
+ cluster-key-slot: 1.1.2
+ debug: 4.4.3
+ denque: 2.1.0
+ lodash.defaults: 4.2.0
+ lodash.isarguments: 3.1.0
+ redis-errors: 1.2.0
+ redis-parser: 3.0.0
+ standard-as-callback: 2.1.0
+ transitivePeerDependencies:
+ - supports-color
+
ipaddr.js@1.9.1: {}
is-array-buffer@3.0.5:
@@ -7369,6 +8929,8 @@ snapshots:
jiti@2.5.1: {}
+ jose@6.2.3: {}
+
js-tokens@4.0.0: {}
js-yaml@4.1.1:
@@ -7381,8 +8943,6 @@ snapshots:
json-parse-even-better-errors@2.3.1: {}
- json-parse-even-better-errors@3.0.2: {}
-
json-schema-traverse@0.4.1: {}
json-schema-traverse@1.0.0: {}
@@ -7404,6 +8964,8 @@ snapshots:
kleur@3.0.3: {}
+ kysely@0.28.16: {}
+
levn@0.4.1:
dependencies:
prelude-ls: 1.2.1
@@ -7466,6 +9028,10 @@ snapshots:
dependencies:
p-locate: 5.0.0
+ lodash.defaults@4.2.0: {}
+
+ lodash.isarguments@3.1.0: {}
+
lodash.merge@4.6.2: {}
lodash.sortby@4.7.0: {}
@@ -7476,8 +9042,6 @@ snapshots:
dependencies:
js-tokens: 4.0.0
- lru-cache@11.2.4: {}
-
lru-cache@5.1.1:
dependencies:
yallist: 3.1.1
@@ -7486,12 +9050,12 @@ snapshots:
dependencies:
yallist: 4.0.0
- lru-cache@7.18.3: {}
-
lucide-react@0.534.0(react@19.1.1):
dependencies:
react: 19.1.1
+ luxon@3.7.2: {}
+
magic-string@0.30.17:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.4
@@ -7585,6 +9149,22 @@ snapshots:
ms@2.1.3: {}
+ msgpackr-extract@3.0.3:
+ dependencies:
+ node-gyp-build-optional-packages: 5.2.2
+ optionalDependencies:
+ '@msgpackr-extract/msgpackr-extract-darwin-arm64': 3.0.3
+ '@msgpackr-extract/msgpackr-extract-darwin-x64': 3.0.3
+ '@msgpackr-extract/msgpackr-extract-linux-arm': 3.0.3
+ '@msgpackr-extract/msgpackr-extract-linux-arm64': 3.0.3
+ '@msgpackr-extract/msgpackr-extract-linux-x64': 3.0.3
+ '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.3
+ optional: true
+
+ msgpackr@1.11.12:
+ optionalDependencies:
+ msgpackr-extract: 3.0.3
+
multer@2.0.2:
dependencies:
append-field: 1.0.0
@@ -7597,6 +9177,8 @@ snapshots:
nanoid@3.3.11: {}
+ nanostores@1.3.0: {}
+
natural-compare@1.4.0: {}
negotiator@0.6.3: {}
@@ -7612,36 +9194,14 @@ snapshots:
react: 19.1.1
react-dom: 19.1.1(react@19.1.1)
- node-releases@2.0.19: {}
-
- node-releases@2.0.27: {}
-
- normalize-package-data@5.0.0:
- dependencies:
- hosted-git-info: 6.1.3
- is-core-module: 2.16.1
- semver: 7.7.2
- validate-npm-package-license: 3.0.4
-
- npm-install-checks@6.3.0:
- dependencies:
- semver: 7.7.2
-
- npm-normalize-package-bin@3.0.1: {}
+ node-abort-controller@3.1.1: {}
- npm-package-arg@10.1.0:
+ node-gyp-build-optional-packages@5.2.2:
dependencies:
- hosted-git-info: 6.1.3
- proc-log: 3.0.0
- semver: 7.7.2
- validate-npm-package-name: 5.0.1
+ detect-libc: 2.0.4
+ optional: true
- npm-pick-manifest@8.0.2:
- dependencies:
- npm-install-checks: 6.3.0
- npm-normalize-package-bin: 3.0.1
- npm-package-arg: 10.1.0
- semver: 7.7.2
+ node-releases@2.0.27: {}
npm-run-path@4.0.1:
dependencies:
@@ -7730,6 +9290,8 @@ snapshots:
dependencies:
p-limit: 3.1.0
+ p-map@7.0.4: {}
+
parent-module@1.0.1:
dependencies:
callsites: 3.1.0
@@ -7738,15 +9300,12 @@ snapshots:
path-exists@4.0.0: {}
+ path-expression-matcher@1.5.0: {}
+
path-key@3.1.1: {}
path-parse@1.0.7: {}
- path-scurry@2.0.1:
- dependencies:
- lru-cache: 11.2.4
- minipass: 7.1.2
-
path-to-regexp@0.1.12: {}
path-to-regexp@8.2.0: {}
@@ -7757,10 +9316,21 @@ snapshots:
pend@1.2.0: {}
+ pg-cloudflare@1.3.0:
+ optional: true
+
+ pg-connection-string@2.12.0: {}
+
pg-int8@1.0.1: {}
+ pg-pool@3.13.0(pg@8.20.0):
+ dependencies:
+ pg: 8.20.0
+
pg-protocol@1.10.3: {}
+ pg-protocol@1.13.0: {}
+
pg-types@2.2.0:
dependencies:
pg-int8: 1.0.1
@@ -7769,12 +9339,32 @@ snapshots:
postgres-date: 1.0.7
postgres-interval: 1.2.0
+ pg@8.20.0:
+ dependencies:
+ pg-connection-string: 2.12.0
+ pg-pool: 3.13.0(pg@8.20.0)
+ pg-protocol: 1.13.0
+ pg-types: 2.2.0
+ pgpass: 1.0.5
+ optionalDependencies:
+ pg-cloudflare: 1.3.0
+
+ pgpass@1.0.5:
+ dependencies:
+ split2: 4.2.0
+
picocolors@1.1.1: {}
picomatch@2.3.1: {}
picomatch@4.0.3: {}
+ pkg-types@2.3.1:
+ dependencies:
+ confbox: 0.2.4
+ exsolve: 1.0.8
+ pathe: 2.0.3
+
possible-typed-array-names@1.1.0: {}
postcss-modules-extract-imports@3.1.0(postcss@8.5.6):
@@ -7825,15 +9415,6 @@ snapshots:
prettier@3.6.2: {}
- proc-log@3.0.0: {}
-
- promise-inflight@1.0.1: {}
-
- promise-retry@2.0.1:
- dependencies:
- err-code: 2.0.3
- retry: 0.12.0
-
prompts@2.4.2:
dependencies:
kleur: 3.0.3
@@ -7924,7 +9505,7 @@ snapshots:
react: 19.1.1
react-dom: 19.1.1(react@19.1.1)
- react-router@7.12.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1):
+ react-router@7.14.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1):
dependencies:
cookie: 1.1.1
react: 19.1.1
@@ -7958,6 +9539,12 @@ snapshots:
tiny-invariant: 1.3.3
tslib: 2.8.1
+ redis-errors@1.2.0: {}
+
+ redis-parser@3.0.0:
+ dependencies:
+ redis-errors: 1.2.0
+
reflect.getprototypeof@1.0.10:
dependencies:
call-bind: 1.0.8
@@ -7995,8 +9582,6 @@ snapshots:
path-parse: 1.0.7
supports-preserve-symlinks-flag: 1.0.0
- retry@0.12.0: {}
-
reusify@1.1.0: {}
rollup@4.50.1:
@@ -8026,9 +9611,11 @@ snapshots:
'@rollup/rollup-win32-x64-msvc': 4.50.1
fsevents: 2.3.3
+ rou3@0.7.12: {}
+
router@2.2.0:
dependencies:
- debug: 4.4.1
+ debug: 4.4.3
depd: 2.0.0
is-promise: 4.0.0
parseurl: 1.3.3
@@ -8088,6 +9675,8 @@ snapshots:
semver@7.7.2: {}
+ semver@7.7.4: {}
+
send@0.19.0:
dependencies:
debug: 2.6.9
@@ -8108,7 +9697,7 @@ snapshots:
send@1.2.0:
dependencies:
- debug: 4.4.1
+ debug: 4.4.3
encodeurl: 2.0.0
escape-html: 1.0.3
etag: 1.8.1
@@ -8144,10 +9733,10 @@ snapshots:
transitivePeerDependencies:
- supports-color
- set-cookie-parser@2.7.1: {}
-
set-cookie-parser@2.7.2: {}
+ set-cookie-parser@3.1.0: {}
+
set-function-length@1.2.2:
dependencies:
define-data-property: 1.1.4
@@ -8230,19 +9819,9 @@ snapshots:
dependencies:
whatwg-url: 7.1.0
- spdx-correct@3.2.0:
- dependencies:
- spdx-expression-parse: 3.0.1
- spdx-license-ids: 3.0.21
-
- spdx-exceptions@2.5.0: {}
-
- spdx-expression-parse@3.0.1:
- dependencies:
- spdx-exceptions: 2.5.0
- spdx-license-ids: 3.0.21
+ split2@4.2.0: {}
- spdx-license-ids@3.0.21: {}
+ standard-as-callback@2.1.0: {}
statuses@2.0.1: {}
@@ -8253,6 +9832,11 @@ snapshots:
es-errors: 1.3.0
internal-slot: 1.1.0
+ stream-browserify@3.0.0:
+ dependencies:
+ inherits: 2.0.4
+ readable-stream: 3.6.2
+
streamsearch@1.1.0: {}
string.prototype.matchall@4.0.12:
@@ -8307,6 +9891,8 @@ snapshots:
strip-json-comments@3.1.1: {}
+ strnum@2.2.3: {}
+
style-loader@4.0.0(webpack@5.105.2(esbuild@0.25.0)):
dependencies:
webpack: 5.105.2(esbuild@0.25.0)
@@ -8357,11 +9943,6 @@ snapshots:
tiny-invariant@1.3.3: {}
- tinyglobby@0.2.14:
- dependencies:
- fdir: 6.5.0(picomatch@4.0.3)
- picomatch: 4.0.3
-
tinyglobby@0.2.15:
dependencies:
fdir: 6.5.0(picomatch@4.0.3)
@@ -8477,12 +10058,6 @@ snapshots:
unpipe@1.0.0: {}
- update-browserslist-db@1.1.3(browserslist@4.25.1):
- dependencies:
- browserslist: 4.25.1
- escalade: 3.2.0
- picocolors: 1.1.1
-
update-browserslist-db@1.2.3(browserslist@4.28.1):
dependencies:
browserslist: 4.28.1
@@ -8518,13 +10093,6 @@ snapshots:
optionalDependencies:
typescript: 5.8.3
- validate-npm-package-license@3.0.4:
- dependencies:
- spdx-correct: 3.2.0
- spdx-expression-parse: 3.0.1
-
- validate-npm-package-name@5.0.1: {}
-
vary@1.1.2: {}
vaul@1.1.2(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.1(react@19.1.1))(react@19.1.1):
@@ -8539,7 +10107,7 @@ snapshots:
vite-node@3.2.4(@types/node@24.1.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4):
dependencies:
cac: 6.7.14
- debug: 4.4.1
+ debug: 4.4.3
es-module-lexer: 1.7.0
pathe: 2.0.3
vite: 7.3.1(@types/node@24.1.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)
@@ -8676,10 +10244,6 @@ snapshots:
dependencies:
isexe: 2.0.0
- which@3.0.1:
- dependencies:
- isexe: 2.0.0
-
word-wrap@1.2.5: {}
wrappy@1.0.2: {}
@@ -8703,4 +10267,4 @@ snapshots:
yocto-queue@0.1.0: {}
- zod@4.0.9: {}
+ zod@4.4.2: {}
diff --git a/public/screenshot-app.png b/public/screenshot-app.png
deleted file mode 100644
index a9f5b973..00000000
Binary files a/public/screenshot-app.png and /dev/null differ
diff --git a/tsconfig.json b/tsconfig.json
index a6b90b7c..da3da7d7 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -17,6 +17,7 @@
"noEmit": true,
"resolveJsonModule": true,
"skipLibCheck": true,
- "strict": true
+ "strict": true,
+ "noFallthroughCasesInSwitch": true
}
}
diff --git a/vite.config.ts b/vite.config.ts
index 4a88d587..ef48e34d 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -5,4 +5,28 @@ import tsconfigPaths from "vite-tsconfig-paths";
export default defineConfig({
plugins: [tailwindcss(), reactRouter(), tsconfigPaths()],
+ // Force a single instance of React/React-DOM. Without dedupe, Radix UI's peer dependency on
+ // React can lead Vite's optimizer to ship two copies (each with its own dispatcher), which
+ // surfaces as "Invalid hook call" / "Cannot read properties of null (reading 'useMemo')" the
+ // first time a route loads a Radix component such as .
+ resolve: {
+ dedupe: ["react", "react-dom"],
+ },
+ optimizeDeps: {
+ include: ["react", "react-dom", "react-dom/client"],
+ },
+ server: {
+ proxy: {
+ "/backend": {
+ target: "http://localhost:3000",
+ changeOrigin: true,
+ rewrite: (path) => path.replace(/^\/backend/, ""),
+ },
+ "/renderer": {
+ target: "http://localhost:8000",
+ changeOrigin: true,
+ rewrite: (path) => path.replace(/^\/renderer/, ""),
+ },
+ },
+ },
});